<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>周小天</title><description>技术分享与实践</description><link>https://blog.wemang.com/</link><language>zh_CN</language><item><title>windows重启命令</title><link>https://blog.wemang.com/posts/tool/windows%E9%87%8D%E5%90%AF%E5%91%BD%E4%BB%A4/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/windows%E9%87%8D%E5%90%AF%E5%91%BD%E4%BB%A4/</guid><description>Windows 快速重启命令是 shutdown /r /t 0，可在 CMD、PowerShell 或 Win+R 运行框中执行，实现立即强制重启</description><pubDate>Fri, 13 Mar 2026 17:20:46 GMT</pubDate><content:encoded>&lt;h2&gt;Windows 快速重启命令是 shutdown /r /t 0，可在 CMD、PowerShell 或 Win+R 运行框中执行，实现立即强制重启。其中 /r 代表重启，/t 0 代表延迟0秒。其他常用参数包括 /f（强制关闭应用）和 /a（取消重启）。&lt;/h2&gt;
&lt;h2&gt;核心重启命令详解&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;立即重启（不强制）： shutdown /r /t 0&lt;/li&gt;
&lt;li&gt;立即强制重启： shutdown /r /f /t 0（即使有未保存文件也强制重启）&lt;/li&gt;
&lt;li&gt;指定时间重启： shutdown /r /t 300（5分钟后重启，300为秒数）&lt;/li&gt;
&lt;li&gt;取消计划的重启： shutdown /a&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;执行方式：&lt;/h2&gt;
&lt;p&gt;1、按下 Win + R 键，输入上述命令并回车。
2、打开“命令提示符”或“PowerShell”输入命令。
3、通过任务管理器（Ctrl+Shift+Esc）点击“文件”-&amp;gt;“运行新任务”输入命令。&lt;/p&gt;
</content:encoded></item><item><title>联系我</title><link>https://blog.wemang.com/posts/pin/</link><guid isPermaLink="true">https://blog.wemang.com/posts/pin/</guid><description>联系周小天，技术交流、合作或其他相关事宜</description><pubDate>Wed, 11 Feb 2026 17:20:46 GMT</pubDate><content:encoded>&lt;h1&gt;联系我&lt;/h1&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;li&gt;问题咨询&lt;/li&gt;
&lt;li&gt;Bug反馈&lt;/li&gt;
&lt;li&gt;商务合作&lt;/li&gt;
&lt;li&gt;或任何你想交流的事情&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;📧 邮箱联系（推荐）&lt;/h2&gt;
&lt;p&gt;点击下方直接发送邮件：&lt;/p&gt;
&lt;p&gt;📮 &lt;strong&gt;邮箱：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;mailto:allen@wemang.com&quot;&gt;allen@wemang.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;或点击：&lt;/p&gt;
&lt;p&gt;👉 &lt;a href=&quot;mailto:allen@wemang.com?subject=%E6%9D%A5%E8%87%AA%E5%8D%9A%E5%AE%A2%E7%9A%84%E8%81%94%E7%B3%BB&amp;amp;body=%E4%BD%A0%E5%A5%BD%EF%BC%8C%E6%88%91%E5%9C%A8%E4%BD%A0%E7%9A%84%E5%8D%9A%E5%AE%A2%E7%9C%8B%E5%88%B0...&quot;&gt;发送邮件给我&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Debian修改SSH登录端口和密码</title><link>https://blog.wemang.com/posts/2025/12/debian%E4%BF%AE%E6%94%B9ssh%E7%99%BB%E5%BD%95%E7%AB%AF%E5%8F%A3%E5%92%8C%E5%AF%86%E7%A0%81/</link><guid isPermaLink="true">https://blog.wemang.com/posts/2025/12/debian%E4%BF%AE%E6%94%B9ssh%E7%99%BB%E5%BD%95%E7%AB%AF%E5%8F%A3%E5%92%8C%E5%AF%86%E7%A0%81/</guid><pubDate>Tue, 09 Dec 2025 09:39:24 GMT</pubDate><content:encoded>&lt;h1&gt;修改SSH端口&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;备份原sshd配置文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;修改sshd配置文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;vim /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;查找Port 22，有可能 Port 22 是注释的(即前面有#号，有的话删掉 # 号)。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 Port 22 下面添加一行 Port 3322  其中3322为你更改SSH后的端口,修改完成后保存。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;重启SSH服务器
重启SSH服务器命令：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有报错的话就生效了，可以 ss -ntl 或 netstat -ntl 查看一下端口。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;防火墙、安全组规则设置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;防火墙可参考对应的服务配置。&lt;/p&gt;
&lt;p&gt;阿里云之类的安全组规则添加SSH新端口规则：
阿里云之类的有安全组之类设置的云服务器一定要在安全组规则里将新端口添加到“入方向”的允许规则。&lt;/p&gt;
&lt;h1&gt;修改密码&lt;/h1&gt;
&lt;p&gt;输入命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo passwd root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;系统将提示您输入并确认新的 root 密码（输入密码时，密码不会显示在屏幕上），终端中内容类似如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;New password: 
Retype new password: 
passwd: password updated successfully
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Windows系统中使用CMD命令计算文件的Hash值</title><link>https://blog.wemang.com/posts/2025/12/windows%E7%B3%BB%E7%BB%9F%E4%B8%AD%E4%BD%BF%E7%94%A8cmd%E5%91%BD%E4%BB%A4%E8%AE%A1%E7%AE%97%E6%96%87%E4%BB%B6%E7%9A%84hash%E5%80%BC/</link><guid isPermaLink="true">https://blog.wemang.com/posts/2025/12/windows%E7%B3%BB%E7%BB%9F%E4%B8%AD%E4%BD%BF%E7%94%A8cmd%E5%91%BD%E4%BB%A4%E8%AE%A1%E7%AE%97%E6%96%87%E4%BB%B6%E7%9A%84hash%E5%80%BC/</guid><pubDate>Wed, 03 Dec 2025 19:39:44 GMT</pubDate><content:encoded>&lt;h2&gt;使用目的&lt;/h2&gt;
&lt;p&gt;为了检查档案是否有被修改或是传输有问题，有时候档案提供者会提供原始档案的杂凑值，当使用者取得档案后，可以用同样的方法执行杂凑，看看是否可以得到与原始提供者相同的杂凑值，假设结果相同，就可以确认档案的完整性。&lt;/p&gt;
&lt;h2&gt;使用方法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;打开CMD命令行窗口&lt;/li&gt;
&lt;li&gt;输入以下命令：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;certutil -hashfile &amp;lt;file_name&amp;gt; [HashAlgorithm]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&amp;lt;file_name&amp;gt;为要计算Hash值的档案名称，[HashAlgorithm]为计算使用的算法，如MD2 MD4 MD5 SHA1 SHA256 SHA384 SHA512等。&lt;/p&gt;
&lt;h4&gt;示例&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;计算D:\test.txt文件的MD5值：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;certutil -hashfile D:\test.txt MD5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/zh-tw/windows-server/administration/windows-commands/certutil&quot;&gt;官方文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>处理nmap扫描结果XML在浏览器无法查看</title><link>https://blog.wemang.com/posts/2025/11/%E5%A4%84%E7%90%86nmap%E6%89%AB%E6%8F%8F%E7%BB%93%E6%9E%9Cxml%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E6%97%A0%E6%B3%95%E6%9F%A5%E7%9C%8B/</link><guid isPermaLink="true">https://blog.wemang.com/posts/2025/11/%E5%A4%84%E7%90%86nmap%E6%89%AB%E6%8F%8F%E7%BB%93%E6%9E%9Cxml%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E6%97%A0%E6%B3%95%E6%9F%A5%E7%9C%8B/</guid><pubDate>Fri, 28 Nov 2025 16:48:55 GMT</pubDate><content:encoded>&lt;h1&gt;问题&lt;/h1&gt;
&lt;p&gt;最近因为软件端口问题，使用nmap进行端口扫描，结果保存为XML文件，在浏览器中打开XML文件查看扫描结果，结果页面空白，啥都没有，打开F12控制台，发现有错误，错误提示如下：
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.41yhx0qokl.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;解决&lt;/h1&gt;
&lt;p&gt;由于浏览器的安全限制，导致无法加载Nmap的nmap.xsl进行渲染，最终找到快速的解决方案是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到nmap.xsl文件，将其保存到本地Nmap安装路径，并修改nmap.xml文件，将xsl文件的路径改为：&lt;code&gt;/nmap.xsl&lt;/code&gt;
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.3ns265ntx2.webp&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;li&gt;启动http服务器，我本地安装了Node，直接使用Node的服务，如果有安装其他的，也可以换成其他服务；
启动命令：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npx http-server .
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;打开浏览器，输入&lt;code&gt;http://localhost:8080/ceshi.html&lt;/code&gt;，就可以看到扫描结果了。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>你可曾想过，直接将BitWarden部署到Cloudflare Worker？</title><link>https://blog.wemang.com/posts/2025/11/warden-worker/</link><guid isPermaLink="true">https://blog.wemang.com/posts/2025/11/warden-worker/</guid><description>warden-worker就是这样一个项目，它将Rust编译为WASM，然后部署到Cloudflare Worker，无需VPS，无需家里云，只需点点鼠标就可免费用上自己的密码托管！</description><pubDate>Fri, 21 Nov 2025 17:07:52 GMT</pubDate><content:encoded>&lt;h1&gt;原理&lt;/h1&gt;
&lt;p&gt;项目参考开源的 &lt;a href=&quot;https://github.com/dani-garcia/vaultwarden&quot;&gt;dani-garcia/vaultwarden: Unofficial Bitwarden compatible server written in Rust, formerly known as bitwarden_rs&lt;/a&gt; 将Rust源码编译为WASM以支持在Cloudflare Worker上运行。其中Worker负责REST API，D1负责存储加密后的数据&lt;/p&gt;
&lt;p&gt;部署的项目： &lt;a href=&quot;https://github.com/zhouzongyan/warden-worker&quot;&gt;https://github.com/zhouzongyan/warden-worker&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;实战&lt;/h1&gt;
&lt;p&gt;打开Cloudflare https://dash.cloudflare.com/&lt;/p&gt;
&lt;p&gt;登录后复制这里的 &lt;strong&gt;账户ID&lt;/strong&gt; （CLOUDFLARE_ACCOUNT_ID）
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.8hgx0jk5en.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;右上角进入配置文件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.szdyml2ce.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;左上角选择API令牌&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.1vz39ihcvr.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击创建令牌
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.1sfhbsomif.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;选择 编辑Cloudflare Workers&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.4qrrfax68c.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;创建后 &lt;strong&gt;复制API 令牌&lt;/strong&gt; （只会展示一次）（CLOUDFLARE_API_TOKEN）
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.86u37e77di.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;回到主页，进入D1数据库&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.96a6kkadou.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;选择 创建数据库&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.6t7k3cwvbt.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;创建完成后，进入，复制 &lt;strong&gt;D1 数据库 ID&lt;/strong&gt;（D1_DATABASE_ID）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于原项目坑点太多（如：依赖不固定版本导致编译报错，必须设置的环境变量不写白，SQL初始化遇到问题直接跳过）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里我Fork并二改了一个我的版本，跟着我的步骤走，包你成功！&lt;/p&gt;
&lt;p&gt;Fork我的仓库（别忘了点个 &lt;strong&gt;Star&lt;/strong&gt; ） &lt;a href=&quot;https://github.com/afoim/warden-worker/&quot;&gt;afoim/warden-worker: A Bitwarden-compatible server for Cloudflare Workers&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在仓库设置中添加上述三个机密环境变量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CLOUDFLARE_ACCOUNT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;D1_DATABASE_ID&lt;/code&gt;
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.175tphw49c.webp&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;点击 Action，运行Build工作流
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.70arysjwqv.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Build结束，全绿&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.5j4mx1g41k.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打开Cloudflare D1，查看数据库表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.39lmdjvqt4.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果这里是空的，我们就手动建表&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.67xwh24col.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;查看这个文件 &lt;a href=&quot;https://github.com/afoim/warden-worker/blob/main/sql/schema.sql&quot;&gt;warden-worker/sql/schema.sql at main · afoim/warden-worker&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;依次将这3个SQL块进行执行（一定要依次，不能一把梭）。每执行一次你应该都能看到新表的出现
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.8s3qtp4nef.webp&quot; alt=&quot;image&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.1hsnindkmu.webp&quot; alt=&quot;image&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.9ddefzzwmn.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;进入Workers&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.1zip78fklc.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;进入 warden-worker&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.7i0tnhl0tu.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;先添加 &lt;strong&gt;自定义域&lt;/strong&gt; ，填你的域名，因为 Worker 默认给的域名国内无法访问
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.60uolqh86h.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再添加&lt;strong&gt;变量与机密&lt;/strong&gt; （注意不要有空格）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ALLOWED_EMAILS&lt;/code&gt; your-email@example.com&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JWT_SECRET&lt;/code&gt; 随机的长字符串&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JWT_REFRESH_SECRET&lt;/code&gt; 随机的长字符串
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.5xb2o0okec.webp&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时打开手机上的 &lt;strong&gt;BitWarden&lt;/strong&gt; 软件，在你的自托管上创建账号即可（注意：密码一经设置将无法更改）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.2h8qvxf8c2.webp&quot; alt=&quot;image&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.7w79ecvyb2.webp&quot; alt=&quot;image&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.77dzuc907i.webp&quot; alt=&quot;image&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.3k8g6tdmzk.webp&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;参考资料：&lt;a href=&quot;https://2x.nz/posts/warden-worker/&quot;&gt;https://2x.nz/posts/warden-worker/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Google 账号修改国家地区方法</title><link>https://blog.wemang.com/posts/2025/11/google-%E8%B4%A6%E5%8F%B7%E4%BF%AE%E6%94%B9%E5%9B%BD%E5%AE%B6%E5%9C%B0%E5%8C%BA%E6%96%B9%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/2025/11/google-%E8%B4%A6%E5%8F%B7%E4%BF%AE%E6%94%B9%E5%9B%BD%E5%AE%B6%E5%9C%B0%E5%8C%BA%E6%96%B9%E6%B3%95/</guid><pubDate>Wed, 19 Nov 2025 15:22:14 GMT</pubDate><content:encoded>&lt;h3&gt;准备工作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一个xxx节点（如果修改为其他地区则需要其他地区的节点）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;确认账号当前的国家或地区&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;访问以下网址&lt;a href=&quot;https://policies.google.com/terms&quot;&gt;https://policies.google.com/terms&lt;/a&gt;，登录自己的账号即可查看账号当前的国家或地区版本。
修改账号当前的国家或地区
访问以下网址&lt;a href=&quot;https://policies.google.com/country-association-form&quot;&gt;https://policies.google.com/country-association-form&lt;/a&gt;，登录自己的账号即可申请修改账号当前的国家或地区版本。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;注意：修改该项每年仅允许一次，即修改成功后，一年之内不允许再次修改。&lt;/strong&gt;&lt;/em&gt;
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251119/image.13m7gkoum1.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;重点来了，以上选择请选最后一个“以上都不是”，参考申请如下：&lt;/p&gt;
&lt;p&gt;我因工作需要用到xxx，请帮我更改到xxx&lt;/p&gt;
&lt;p&gt;转自:&lt;a href=&quot;https://linux.do/t/topic/1186531&quot;&gt;https://linux.do/t/topic/1186531&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>查询 Github 注册时间</title><link>https://blog.wemang.com/posts/2025/11/%E6%9F%A5%E8%AF%A2-github-%E6%B3%A8%E5%86%8C%E6%97%B6%E9%97%B4/</link><guid isPermaLink="true">https://blog.wemang.com/posts/2025/11/%E6%9F%A5%E8%AF%A2-github-%E6%B3%A8%E5%86%8C%E6%97%B6%E9%97%B4/</guid><pubDate>Mon, 17 Nov 2025 20:05:21 GMT</pubDate><content:encoded>&lt;p&gt;:::note
查询 Github 注册时间
:::&lt;/p&gt;
&lt;p&gt;访问 &lt;code&gt;https://api.github.com/users/用户名&lt;/code&gt;,例如： &lt;a href=&quot;https://api.github.com/users/zhouzongyang&quot;&gt;https://api.github.com/users/zhouzongyang&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;响应结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;login&quot;: &quot;zhouzongyan&quot;,
  &quot;id&quot;: 18110319,
  &quot;node_id&quot;: &quot;MDQ6VXNlcjE4MTEwMzE5&quot;,
  &quot;avatar_url&quot;: &quot;https://avatars.githubusercontent.com/u/18110319?v=4&quot;,
  &quot;gravatar_id&quot;: &quot;&quot;,
  &quot;url&quot;: &quot;https://api.github.com/users/zhouzongyan&quot;,
  &quot;html_url&quot;: &quot;https://github.com/zhouzongyan&quot;,
  &quot;followers_url&quot;: &quot;https://api.github.com/users/zhouzongyan/followers&quot;,
  &quot;following_url&quot;: &quot;https://api.github.com/users/zhouzongyan/following{/other_user}&quot;,
  &quot;gists_url&quot;: &quot;https://api.github.com/users/zhouzongyan/gists{/gist_id}&quot;,
  &quot;starred_url&quot;: &quot;https://api.github.com/users/zhouzongyan/starred{/owner}{/repo}&quot;,
  &quot;subscriptions_url&quot;: &quot;https://api.github.com/users/zhouzongyan/subscriptions&quot;,
  &quot;organizations_url&quot;: &quot;https://api.github.com/users/zhouzongyan/orgs&quot;,
  &quot;repos_url&quot;: &quot;https://api.github.com/users/zhouzongyan/repos&quot;,
  &quot;events_url&quot;: &quot;https://api.github.com/users/zhouzongyan/events{/privacy}&quot;,
  &quot;received_events_url&quot;: &quot;https://api.github.com/users/zhouzongyan/received_events&quot;,
  &quot;type&quot;: &quot;User&quot;,
  &quot;user_view_type&quot;: &quot;public&quot;,
  &quot;site_admin&quot;: false,
  &quot;name&quot;: &quot;Allen&quot;,
  &quot;company&quot;: null,
  &quot;blog&quot;: &quot;https://chn.cc&quot;,
  &quot;location&quot;: null,
  &quot;email&quot;: null,
  &quot;hireable&quot;: null,
  &quot;bio&quot;: &quot;道可道，非常可道；名可名，非常可名！&quot;,
  &quot;twitter_username&quot;: null,
  &quot;public_repos&quot;: 135,
  &quot;public_gists&quot;: 3,
  &quot;followers&quot;: 8,
  &quot;following&quot;: 23,
  &quot;created_at&quot;: &quot;2016-03-28T05:40:43Z&quot;,
  &quot;updated_at&quot;: &quot;2025-11-14T01:50:23Z&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;created_at&lt;/code&gt; 为注册时间&lt;/p&gt;
</content:encoded></item><item><title>Claude Code Router安装和使用</title><link>https://blog.wemang.com/posts/2025/11/claude-code-router%E5%AE%89%E8%A3%85%E5%92%8C%E4%BD%BF%E7%94%A8/</link><guid isPermaLink="true">https://blog.wemang.com/posts/2025/11/claude-code-router%E5%AE%89%E8%A3%85%E5%92%8C%E4%BD%BF%E7%94%A8/</guid><pubDate>Sat, 15 Nov 2025 12:03:00 GMT</pubDate><content:encoded>&lt;p&gt;:::note
Claude Code Router 就是这样一款强大的 Claude Code 开源代理工具，它可以将 Claude Code 的请求路由到不同的大模型，并支持自定义任何请求。
:::&lt;/p&gt;
&lt;h3&gt;介绍&lt;/h3&gt;
&lt;p&gt;主要功能如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模型路由：根据您的需求将请求路由到不同的模型（比如：后台任务、思考、长上下文）。&lt;/li&gt;
&lt;li&gt;多提供商支持：支持 OpenRouter、DeepSeek、Ollama、Gemini、Volcengine 和 SiliconFlow 等各种模型提供商。&lt;/li&gt;
&lt;li&gt;请求/响应转换：使用转换器为不同的提供商自定义请求和响应。&lt;/li&gt;
&lt;li&gt;动态模型切换：在 Claude Code 中使用 /model 命令动态切换模型。
GitHub Actions 集成：在您的 GitHub 工作流程中触发 Claude Code 任务。&lt;/li&gt;
&lt;li&gt;插件系统：使用自定义转换器扩展功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;p&gt;1、安装Claude Code
安装之前记得先安装Git&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   npm install -g @anthropic-ai/claude-code      
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，使用&lt;code&gt;claude –version&lt;/code&gt;验证是否安装成功&lt;/p&gt;
&lt;p&gt;2、安装Claude Code Router
Claude Code Router 开源地址：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/musistudio/claude-code-router&quot;&gt;https://github.com/musistudio/claude-code-router&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;执行安装命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -g @musistudio/claude-code-router
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装后，创建并配置你的 &lt;code&gt;~/.claude-code-router/config.json&lt;/code&gt; 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;LOG&quot;: false,
  &quot;HOST&quot;: &quot;0.0.0.0&quot;,
  &quot;API_TIMEOUT_MS&quot;: 600000,
  &quot;Providers&quot;: [
    {
      &quot;name&quot;: &quot;deepseek&quot;,
      &quot;api_base_url&quot;: &quot;https://api.deepseek.com/chat/completions&quot;,
      &quot;api_key&quot;: &quot;sk-xxx&quot;,
      &quot;models&quot;: [&quot;deepseek-chat&quot;, &quot;deepseek-reasoner&quot;],
      &quot;transformer&quot;: {
        &quot;use&quot;: [&quot;deepseek&quot;],
        &quot;deepseek-chat&quot;: {
          &quot;use&quot;: [&quot;tooluse&quot;]
        }
      }
    },
    {
      &quot;name&quot;: &quot;gemini&quot;,
      &quot;api_base_url&quot;: &quot;https://generativelanguage.googleapis.com/v1beta/models/&quot;,
      &quot;api_key&quot;: &quot;xx&quot;,
      &quot;models&quot;: [&quot;gemini-2.5-flash&quot;, &quot;gemini-2.5-pro&quot;],
      &quot;transformer&quot;: {
        &quot;use&quot;: [&quot;gemini&quot;]
      }
    }
  ],
  &quot;Router&quot;: {
    &quot;default&quot;: &quot;deepseek,deepseek-chat&quot;,
    &quot;background&quot;: &quot;deepseek,deepseek-chat&quot;,
    &quot;think&quot;: &quot;deepseek,deepseek-reasoner&quot;,
    &quot;longContext&quot;: &quot;gemini,gemini-2.5-pro&quot;,
    &quot;longContextThreshold&quot;: 60000,
    &quot;webSearch&quot;: &quot;gemini,gemini-2.5-flash&quot;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&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;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;PROXY_URL&lt;/td&gt;
&lt;td&gt;可选&lt;/td&gt;
&lt;td&gt;API 请求代理地址&lt;/td&gt;
&lt;td&gt;&quot;http://127.0.0.1:7890&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LOG&lt;/td&gt;
&lt;td&gt;可选&lt;/td&gt;
&lt;td&gt;启用日志，文件位于&lt;/td&gt;
&lt;td&gt;$HOME/.claude-code-router.log	true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APIKEY&lt;/td&gt;
&lt;td&gt;可选&lt;/td&gt;
&lt;td&gt;API 访问密钥，需在 Authorization 或x-api-key 请求头传入&lt;/td&gt;
&lt;td&gt;&quot;your-secret-key&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HOST&lt;/td&gt;
&lt;td&gt;可选&lt;/td&gt;
&lt;td&gt;服务主机地址；未设置 APIKEY 时默认强制 127.0.0.1&lt;/td&gt;
&lt;td&gt;&quot;0.0.0.0&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NON_INTERACTIVE_MODE&lt;/td&gt;
&lt;td&gt;可选&lt;/td&gt;
&lt;td&gt;非交互模式，适配 CI/CD 等自动化环境&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Providers&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;Router&lt;/td&gt;
&lt;td&gt;可选&lt;/td&gt;
&lt;td&gt;路由规则：&amp;lt;br/&amp;gt; - default：常规任务默认模&amp;lt;br/&amp;gt;- background：后台任务模型（可用小型本地模型降成本&amp;lt;br/&amp;gt;- think：推理密集型任务模型&amp;lt;br/&amp;gt;- longContext：长上下文模型（&amp;gt; 60K token）&amp;lt;br/&amp;gt;- longContextThreshold（可选）：触发长上下文的 token 阈值，默认 60000&amp;lt;br/&amp;gt;- webSearch：网络搜索模型（openrouter 需加 :online 后缀）&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API_TIMEOUT_MS&lt;/td&gt;
&lt;td&gt;可选&lt;/td&gt;
&lt;td&gt;API 请求超时时间（毫秒）&lt;/td&gt;
&lt;td&gt;30000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;使用 Router 运行 Claude Code&lt;/h3&gt;
&lt;p&gt;在某个项目下使用 &lt;code&gt;ccr code&lt;/code&gt; 命令来启动 Claude Code，启动 Claude Code 成功后，主界面会显示代理的 API 信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image.2a5j0r0amj.webp&quot; alt=&quot;启动&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意：修改配置文件后，需要重启服务使配置生效：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ccr restart&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;UI 模式 (Beta)&lt;/h3&gt;
&lt;p&gt;为了获得更直观的体验，可以使用 UI 模式来管理配置，命令如下：
&lt;code&gt;ccr ui&lt;/code&gt;
这个命令会打开一个基于 Web 的界面：
&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20251127/image-1.60uolzo8qb.png&quot; alt=&quot;UI&quot; /&gt;
Claude Code 本身体验极好，但受限于昂贵的订阅费用和网络环境，想要长期使用成本并不低。&lt;/p&gt;
&lt;p&gt;而 Claude Code Router 提供了一种更灵活、低成本的替代方案：保留 Claude Code 的使用习惯，同时自由接入任意大模型提供商，实现模型的路由、切换与请求适配，大大提升了自由度与可控性。&lt;/p&gt;
</content:encoded></item><item><title>nvm安装和配置</title><link>https://blog.wemang.com/posts/node/nvvm%E5%AE%89%E8%A3%85%E5%92%8C%E9%85%8D%E7%BD%AE/</link><guid isPermaLink="true">https://blog.wemang.com/posts/node/nvvm%E5%AE%89%E8%A3%85%E5%92%8C%E9%85%8D%E7%BD%AE/</guid><pubDate>Fri, 14 Nov 2025 11:08:00 GMT</pubDate><content:encoded>&lt;h1&gt;介绍&lt;/h1&gt;
&lt;p&gt;nvm：nodejs 版本管理工具，类似于python的Miniconda，一个 nvm 可以管理很多 node 版本和 npm 版本。&lt;/p&gt;
&lt;h1&gt;安装&lt;/h1&gt;
&lt;p&gt;1、下载 &lt;a href=&quot;https://github.com/coreybutler/nvm-windows/releases&quot;&gt;https://github.com/coreybutler/nvm-windows/releases&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;2、选择exe下载后无脑下一步即可，exe版本回自动配置环境变量，安装过程中可以选择安装位置(建议安装到非中文路径)&lt;/p&gt;
&lt;p&gt;3、打开cmd验证是否安装成功&lt;/p&gt;
&lt;h1&gt;常用命令&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;1. nvm arch：
   该命令显示 Node 正在以 32 位或 64 位模式运行。
2. nvm current：
   该命令显示当前活动的 Node 版本。
3. nvm debug：
   该命令检查 NVM4W 进程是否存在已知问题，并可帮助解决与 NVM 相关的任何问题。
4. nvm install &amp;lt;version&amp;gt; [arch]：
   该命令用于安装特定版本的 Node。您可以指定版本号，使用 &quot;latest&quot; 获取最新的当前版本，或使用 &quot;lts&quot; 获取最新的 LTS 版本。可选地，您可以指定安装 32 位或 64 位版本。
5. nvm list [available]：
   该命令列出已安装的 Node.js 版本。在末尾添加 &quot;available&quot; 将显示可安装的版本。此命令还可以别名为 &quot;ls&quot;。
6. nvm on：
   该命令启用 Node.js 版本管理。
7. nvm off：
   该命令禁用 Node.js 版本管理。
8. nvm proxy [url]：
   该命令设置用于下载的代理。将 [url] 留空将显示当前代理。将 [url] 设置为 &quot;none&quot; 将移除代理。
9. nvm node_mirror [url]：
   该命令设置 Node 镜像。默认情况下，它设置为 https://nodejs.org/dist/。将 [url] 留空将使用默认 URL。
10. nvm npm_mirror [url]：
    该命令设置 NPM 镜像。默认情况下，它设置为 https://github.com/npm/cli/archive/。将 [url] 留空将使用默认 URL。
11. nvm uninstall &amp;lt;version&amp;gt;：
    该命令卸载特定版本的 Node。
12. nvm use [version] [arch]：
    该命令切换到使用指定的 Node 版本。您可以使用 &quot;latest&quot;、&quot;lts&quot; 或 &quot;newest&quot; 作为快捷方式。可选地，您可以指定 32 位或 64 位架构。使用 &quot;nvm use &amp;lt;arch&amp;gt;&quot; 将继续使用所选版本，但切换到指定的位模式。
13. nvm root [path]：
    该命令设置 NVM 存储不同版本的 Node.js 的目录。如果未设置 [path]，将显示当前根目录。
14. nvm [--]version：
    该命令显示 Windows 版本的当前运行的 NVM 版本。也可以别名为 &quot;v&quot;。
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Node全局配置&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;npm config set prefix &quot;D:\Nodejs\node_global&quot;
npm config set cache &quot;D:\Nodejs\node_cache&quot;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>PowerShell因为在此系统上禁止运行脚本，解决方法</title><link>https://blog.wemang.com/posts/tool/powershell%E5%9B%A0%E4%B8%BA%E5%9C%A8%E6%AD%A4%E7%B3%BB%E7%BB%9F%E4%B8%8A%E7%A6%81%E6%AD%A2%E8%BF%90%E8%A1%8C%E8%84%9A%E6%9C%AC%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/powershell%E5%9B%A0%E4%B8%BA%E5%9C%A8%E6%AD%A4%E7%B3%BB%E7%BB%9F%E4%B8%8A%E7%A6%81%E6%AD%A2%E8%BF%90%E8%A1%8C%E8%84%9A%E6%9C%AC%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95/</guid><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;报错详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;PS E:\code&amp;gt; hexo server
hexo : 无法加载文件 C:\Users\Administrator\AppData\Roaming\npm\hexo.ps1，
因为在此系统上禁止运行脚本。有关详细信息，请参阅
https:/go.microsoft.com/fwlink/?LinkID=135170 中的about_Execution_Policies。
所在位置 行:1 字符: 1
+ hexo new &quot;PowerShell：因为在此系统上禁止运行脚本，解决方法&quot;
+
    + CategoryInfo          : SecurityError: (:) []，PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计算机上启动 Windows PowerShell 时，执行策略很可能是 Restricted（默认设置）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Restricted 执行策略不允许任何脚本运行。&lt;/li&gt;
&lt;li&gt;AllSigned 和 RemoteSigned 执行策略可防止 Windows PowerShell 运行没有数字签名的脚本。
计算机上的现用执行策略，打开 PowerShell 然后输入 ：get-executionpolicy&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;PS C:\WINDOWS\system32&amp;gt; get-executionpolicy
Restricted
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置
以管理员身份打开 PowerShell 输入 &lt;code&gt;set-executionpolicy remotesigned&lt;/code&gt;，并输入 Y。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS C:\WINDOWS\system32&amp;gt; set-executionpolicy remotesigned

执行策略更改
执行策略可帮助你防止执行不信任的脚本。更改执行策略可能会产生安全风险，如 https:/go.microsoft.com/fwlink/?LinkID=135170
中的 about_Execution_Policies 帮助主题所述。是否要更改执行策略?
[Y] 是(Y)  [A] 全是(A)  [N] 否(N)  [L] 全否(L)  [S] 暂停(S)  [?] 帮助 (默认值为“N”): y
PS C:\WINDOWS\system32&amp;gt; get-executionpolicy
RemoteSigned
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jianshu.com/p/4eaad2163567&quot;&gt;PowerShell：因为在此系统上禁止运行脚本，解决方法&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Nginx+ CertBot配置HTTPS泛域名证书(Debian)</title><link>https://blog.wemang.com/posts/tool/nginxcertbot%E9%85%8D%E7%BD%AEhttps%E6%B3%9B%E5%9F%9F%E5%90%8D%E8%AF%81%E4%B9%A6debian/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/nginxcertbot%E9%85%8D%E7%BD%AEhttps%E6%B3%9B%E5%9F%9F%E5%90%8D%E8%AF%81%E4%B9%A6debian/</guid><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;安装 Nginx&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;apt install nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;配置证书&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;# 1. 安装certbot
apt-get install certbot python3-certbot-nginx

# 2. 获取证书
# 自动配置, --nginx-server-root指定nginx配置文件目录
certbot --nginx --nginx-server-root /usr/local/nginx-1.23.3/conf
# 指定域名, example.com换成自己的域名
certbot --nginx -d example.com -d www.example.com


# 3. 自动续约
certbot renew --dry-run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完后 nginx.conf 会自动加上 SSL 相关配置&lt;/p&gt;
&lt;h1&gt;常见问题&lt;/h1&gt;
&lt;p&gt;问题一、第 2 步获取证书报错:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error while running nginx -c /usr/local/nginx-1.25.4/conf/nginx.conf -t.

nginx: the configuration file /usr/local/nginx-1.25.4/conf/nginx.conf syntax is ok
2024/03/11 10:42:44 [emerg] 1985275#1985275: open() &quot;/usr/share/nginx/logs/xxx_admin.access.log&quot; failed (2: No such file or directory)
nginx: configuration file /usr/local/nginx-1.25.4/conf/nginx.conf test failed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建&lt;code&gt;/usr/share/nginx/logs&lt;/code&gt;目录， 重新执行第 2 步命令&lt;/p&gt;
&lt;p&gt;问题二、执行&lt;code&gt;certbot --nginx --nginx-server-root /usr/local/nginx-1.23.3/conf&lt;/code&gt;命令报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Saving debug log to /var/log/letsencrypt/letsencrypt.log
The nginx plugin is not working; there may be problems with your existing configuration.
The error was: NoInstallationError(&quot;Could not find a usable &apos;nginx&apos; binary. Ensure nginx exists, the binary is executable, and your PATH is set correctly.&quot;,)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决方案:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln -s /usr/local/nginx-1.23.3/sbin/nginx /usr/bin/nginx
ln -s /usr/local/nginx-1.23.3/conf/ /etc/nginx
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Docusaurus中使用Algolia的DocSearch搜索功能</title><link>https://blog.wemang.com/posts/tool/docusaurus%E4%B8%AD%E4%BD%BF%E7%94%A8algolia%E7%9A%84docsearch%E6%90%9C%E7%B4%A2%E5%8A%9F%E8%83%BD/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/docusaurus%E4%B8%AD%E4%BD%BF%E7%94%A8algolia%E7%9A%84docsearch%E6%90%9C%E7%B4%A2%E5%8A%9F%E8%83%BD/</guid><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言: Algolia 是什么？&lt;/h2&gt;
&lt;p&gt;Algolia 是一个搜索、推荐服务平台，可以通过简单的配置来为站点添加全文检索功能&lt;/p&gt;
&lt;p&gt;基本原理：&lt;/p&gt;
&lt;p&gt;通过爬虫对目标网站的内容创建 Records (记录), 在用户搜索时调用接口返回相关内容&lt;/p&gt;
&lt;h2&gt;一. 需求描述&lt;/h2&gt;
&lt;p&gt;为网站添加 实时搜索, 采用 Docusaurus2 官方支持的 Algolia DocSearch&lt;/p&gt;
&lt;p&gt;Docsearch 每周一次爬取网站 (可在网页界面上配置具体时间), 并将所有内容汇总到一个 Algolia 索引中&lt;/p&gt;
&lt;p&gt;随后，前端页面会调用 Algolia API 来直接查询这些内容&lt;/p&gt;
&lt;p&gt;Docusaurus 搜索功能文档&lt;/p&gt;
&lt;h2&gt;二. 准备工作&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Docsearch 官网申请
前置条件:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;准备项目域名地址 - 本案例: https://wiki.chn.gg/&lt;/p&gt;
&lt;p&gt;备注
如没有 服务器和域名 也可用 GitHub Pages&lt;/p&gt;
&lt;p&gt;前置条件准备完成后, 就可到 Docsearch 注册&lt;/p&gt;
&lt;p&gt;提交后大约 2 天内会收到 反馈邮件, 通知注册成功&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取 Application ID &amp;amp; API Keys
前往 Algolia 官网, 登录账户 创建 Application&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;设置 Application 名称, 选择免费计划&lt;/p&gt;
&lt;p&gt;最后选择响应速度快的服务后, 创建成功 ✅&lt;/p&gt;
&lt;p&gt;控制台打开 设置页面，点击 API keys&lt;/p&gt;
&lt;p&gt;找到 接下来本地配置需要的数据&lt;/p&gt;
&lt;h2&gt;三. 本地 Algolia Docsearch 配置&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;.env (键值不带双引号)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;APPLICATION_ID=Application ID
API_KEY=Admin API Key # 务必确认, 这是坑点 不要用 &apos;Write API Key&apos; 或者 &apos;Search API Key&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;docusaurus.config.js&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
	// ...
	presets: [
		[
			// ...
			&apos;classic&apos;,
			/** @type {import(&apos;@docusaurus/preset-classic&apos;).Options} */
			({
				// 这个插件会为你的站点创建一个站点地图
				// 以便搜索引擎的爬虫能够更准确地爬取你的网站
				sitemap: {
					changefreq: &apos;weekly&apos;,
					priority: 0.5,
					ignorePatterns: [&apos;/tags/**&apos;],
					filename: &apos;sitemap.xml&apos;,
				},
			}),
		],
	],
	// ...
	themeConfig: {
		// ...
		algolia: {
			appId: &apos;YOUR_APP_ID&apos;, // Application ID
			//  公开 API密钥：提交它没有危险
			apiKey: &apos;YOUR_SEARCH_API_KEY&apos;, //  Search-Only API Key
			indexName: &apos;YOUR_INDEX_NAME&apos;,
		},
	},
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docusaurus.io/zh-CN/docs/search#using-algolia-docsearch&quot;&gt;Algolia DocSearch 插件文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docusaurus.io/zh-CN/docs/api/plugins/@docusaurus/plugin-sitemap&quot;&gt;sitemap 插件文档&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docsearch-config.json&lt;/code&gt; (爬虫配置文件)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;需修改 3 处:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;index_name&lt;/li&gt;
&lt;li&gt;start_urls&lt;/li&gt;
&lt;li&gt;sitemap_urls&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;index_name&quot;: &quot;wiki&quot;,
	&quot;start_urls&quot;: [&quot;https://wiki.chn.gg/&quot;],
	&quot;sitemap_urls&quot;: [&quot;https://wiki.chn.gg/sitemap.xml&quot;],
	&quot;sitemap_alternate_links&quot;: true,
	&quot;stop_urls&quot;: [&quot;/tests&quot;],
	&quot;selectors&quot;: {
		&quot;lvl0&quot;: {
			&quot;selector&quot;: &quot;(//ul[contains(@class,&apos;menu__list&apos;)]//a[contains(@class, &apos;menu__link menu__link--sublist menu__link--active&apos;)]/text() | //nav[contains(@class, &apos;navbar&apos;)]//a[contains(@class, &apos;navbar__link--active&apos;)]/text())[last()]&quot;,
			&quot;type&quot;: &quot;xpath&quot;,
			&quot;global&quot;: true,
			&quot;default_value&quot;: &quot;Documentation&quot;
		},
		&quot;lvl1&quot;: &quot;header h1&quot;,
		&quot;lvl2&quot;: &quot;article h2&quot;,
		&quot;lvl3&quot;: &quot;article h3&quot;,
		&quot;lvl4&quot;: &quot;article h4&quot;,
		&quot;lvl5&quot;: &quot;article h5, article td:first-child&quot;,
		&quot;lvl6&quot;: &quot;article h6&quot;,
		&quot;text&quot;: &quot;article p, article li, article td:last-child&quot;
	},
	&quot;strip_chars&quot;: &quot; .,;:#&quot;,
	&quot;custom_settings&quot;: {
		&quot;separatorsToIndex&quot;: &quot;_&quot;,
		&quot;attributesForFaceting&quot;: [&quot;language&quot;, &quot;version&quot;, &quot;type&quot;, &quot;docusaurus_tag&quot;],
		&quot;attributesToRetrieve&quot;: [&quot;hierarchy&quot;, &quot;content&quot;, &quot;anchor&quot;, &quot;url&quot;, &quot;url_without_anchor&quot;, &quot;type&quot;]
	},
	&quot;js_render&quot;: true,
	&quot;conversation_id&quot;: [&quot;833762294&quot;],
	&quot;nb_hits&quot;: 46250
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/algolia/docsearch-configs/blob/master/configs/docusaurus-2.json&quot;&gt;Algolia 官方用例&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;四. 执行爬虫程序 - docsearch-scraper&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;以下两种 爬虫方式任选其一即可 (推荐使用 GitHub Actions)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1. 本地 执行爬虫&lt;/h3&gt;
&lt;p&gt;前置条件:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Docker&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;jq - 轻量级命令行 JSON 处理器&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 brew 安装最新版的 jq&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;jq 安装完成后, 在命令行执行 爬虫脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -it --env-file=.env -e &quot;CONFIG=$(cat docsearch-config.json | jq -r tostring)&quot; algolia/docsearch-scraper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待 容器运行完成, 如下即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
Getting https://wiki.chn.gg/docs/react/hooks/custom-hooks from selenium
Getting https://wiki.chn.gg/docs/react/hooks/useMemo from selenium
Getting https://wiki.chn.gg/docs/react/hooks/useCallback from selenium
Getting https://wiki.chn.gg/docs/javascript/versions/es-2016 from selenium
Getting https://wiki.chn.gg/docs/javascript/versions/es-2015 from selenium
&amp;gt; DocSearch: https://wiki.chn.gg/docs/plugins-and-libraries/big-screen/ 17 records)
&amp;gt; DocSearch: https://wiki.chn.gg/docs/server/nginx/nginx-forward-proxy-vs-reverse-proxy/ 8 records)
&amp;gt; DocSearch: https://wiki.chn.gg/docs/category/caddy/ 3 records)
&amp;gt; DocSearch: https://wiki.chn.gg/docs/category/nginx/ 5 records)

Nb hits: 1369
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. GitHub Actions 执行爬虫&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;.github/workflows/&lt;/code&gt; 文件夹下 创建 &lt;code&gt;docsearch-scraper.yml&lt;/code&gt;, 用来定义 GitHub Actions 工作流
docsearch-scraper.yml(使用 vercel 发布)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: 索引爬虫 docsearch-scraper

on:
    deployment_status:
#   push:
#     branches: [main]
#   pull_request:
#     branches: [main]

jobs:
    scan:
        if: github.event_name == &apos;deployment_status&apos; &amp;amp;&amp;amp; github.event.deployment_status.state == &apos;success&apos;
        runs-on: ubuntu-latest

        steps:
            - name: Sleep for 10 seconds
              run: sleep 10s
              shell: bash

            - name: Checkout repo
              uses: actions/checkout@v3

            - name: Run scraper
              env:
                  APPLICATION_ID: ${{ secrets.APPLICATION_ID }}
                  API_KEY: ${{ secrets.API_KEY }}
              run: |
                  CONFIG=&quot;$(cat docsearch-config.json)&quot;
                  docker run -i --rm \
                          -e APPLICATION_ID=$APPLICATION_ID \
                          -e API_KEY=$API_KEY \
                          -e CONFIG=&quot;${CONFIG}&quot; \
                          algolia/docsearch-scraper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;docsearch-scraper.yml(使用 github pages 发布)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: 索引爬虫 docsearch-scraper

on:
    push:
        branches: [main]
    pull_request:
        branches: [main]

jobs:
    scan:
        runs-on: ubuntu-latest

        steps:
            - name: Sleep for 10 seconds
              run: sleep 10s
              shell: bash

            - name: Checkout repo
              uses: actions/checkout@v3

            - name: Run scraper
              env:
                  APPLICATION_ID: ${{ secrets.APPLICATION_ID }}
                  API_KEY: ${{ secrets.API_KEY }}
              run: |
                  CONFIG=&quot;$(cat docsearch-config.json)&quot;
                  docker run -i --rm \
                          -e APPLICATION_ID=$APPLICATION_ID \
                          -e API_KEY=$API_KEY \
                          -e CONFIG=&quot;${CONFIG}&quot; \
                          algolia/docsearch-scraper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 GitHub 的 Secrets 创建&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;APPLICATION_ID&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;API_KEY&lt;/code&gt; — Admin API Key&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当使用 Git 推送项目到 GitHub 时, Actions 就会自动执行 爬虫任务&lt;/p&gt;
&lt;p&gt;坑点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;官网邮件通知 创建好的项目 是没有 管理员权限的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要在个人主页, 重新创建项目, 注意起名字 不要冲突&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;index_name 需要三处对齐
docsearch-config.json - index_name
docusaurus.config.js - indexName
Algolia 官网 的项目名称 - Application
如: wiki&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;示例代码&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/didilinkin/didilinkin-website&quot;&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://wiki.chn.gg/&quot;&gt;实际效果 - 点击右上角 &quot;搜索&quot;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;常见问题&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;algoliasearch.exceptions.RequestException: Method not allowed with this API key&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;提示
这个错误通常表示您在使用 Algolia Search API 时使用了无效的 API 密钥或 API 密钥权限不允许使用该请求方法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分析
使用的 key 不对 - 当前使用的也许是 Search-Only API Key&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解决方法
改为使用 Admin API Key&lt;/p&gt;
&lt;p&gt;2.Error: Process completed with exit code 3.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;提示
由于尝试使用 Algolia Search API 更新对象时所使用的 API 密钥缺少必要的权限&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;确认您正在使用具有正确权限的 Algolia Search API 密钥来更新对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分析
使用的 key 权限不对 - 当前使用的也许是 Write API Key&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解决方法
改为使用 Admin API Key&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;本地执行 爬虫脚本, 报错: Error: Cannot find module &apos;winston&apos;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 yarn 重新安装 winston&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;yarn global remove winston

yarn global add winston
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;本地执行 爬虫脚本, 报错: &lt;code&gt;Error: { cli } was removed in winston@3.0.0&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 查看本地的 全局依赖是否存在 jq (yarn)
yarn global list --depth=0

# 删除 jq依赖
yarn global remove jq
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;本地执行 爬虫脚本, 报错: zsh: command not found: jq
确保本地 npm, yarn, pnpm 的全局依赖中没有 jq, 使用 brew 安装&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 安装最新版 jq
brew install --HEAD jq
# 链接 最新版 jq
brew link jq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考链接&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docsearch.algolia.com/docs/legacy/run-your-own/#running-the-crawler-from-the-code-base&quot;&gt;官网文档 - Run your own&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/stedolan/jq/wiki/Installation#zero-install&quot;&gt;jq - Installation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>RustDesk远程桌面工具自建服务器教程</title><link>https://blog.wemang.com/posts/tool/rustdesk%E8%BF%9C%E7%A8%8B%E6%A1%8C%E9%9D%A2%E5%B7%A5%E5%85%B7%E8%87%AA%E5%BB%BA%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%95%99%E7%A8%8B/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/rustdesk%E8%BF%9C%E7%A8%8B%E6%A1%8C%E9%9D%A2%E5%B7%A5%E5%85%B7%E8%87%AA%E5%BB%BA%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%95%99%E7%A8%8B/</guid><pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;简介
RustDesk，工具如其名，基于高效的 Rust 语言构建的开源远程桌面工具。&lt;/p&gt;
&lt;p&gt;优势：&lt;/p&gt;
&lt;p&gt;（1）轻量：不论服务端还是客户端、不论哪个平台，软件小巧、功能完备。&lt;/p&gt;
&lt;p&gt;（2）全平台支持：支持 Android、Linux、Windows 任意双向控制，iOS 单向控制。&lt;/p&gt;
&lt;p&gt;（3）安全可控：软件开源，服务端自建，通信加密。&lt;/p&gt;
&lt;p&gt;（4）带宽高效：仅需 2-3M 即可流畅 1080P，支持 TCP 打洞端对端 P2P 连接。&lt;/p&gt;
&lt;p&gt;RustDesk 官网：&lt;a href=&quot;https://rustdesk.com/zh/&quot;&gt;https://rustdesk.com/zh/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;GitHub：&lt;a href=&quot;https://github.com/rustdesk/rustdesk&quot;&gt;https://github.com/rustdesk/rustdesk&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;运行原理
&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240621/image.969ls0ywpf.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;RustDesk 自建服务器运行原理&lt;/p&gt;
&lt;h3&gt;自建服务器&lt;/h3&gt;
&lt;p&gt;免费的公共服务器非常卡，在国内也许网速会很慢或者无法访问。&lt;/p&gt;
&lt;p&gt;RustDesk 的优势在于可以使用自己的服务器，推荐使用国内主流云产品厂商，个人使用的腾讯云搭建服务器，价格不贵可以接受，相比于远程流畅度来说物超所值，且服务器唯自己所有，不用担心第三方远控软件造成的信息安全问题。&lt;/p&gt;
&lt;h3&gt;服务器要求&lt;/h3&gt;
&lt;p&gt;硬件要求很低，最低配置的云服务器就可以了，CPU 和内存要求都是最小的。&lt;/p&gt;
&lt;p&gt;关于网络大小，如果控制端和被控端位于同一网段下时，不需要通过 relay server 中转，直接建立内网链接。&lt;/p&gt;
&lt;p&gt;如果 TCP 打洞直连失败，就要耗费中继流量，一个中继连接的流量在 30k-3M 每秒之间（1920x1080 屏幕），取决于清晰度设置和画面变化。如果只是办公需求，平均在 100K/s。&lt;/p&gt;
&lt;p&gt;非直连情况下，播放全屏视频（1920x1080 屏幕）实测近两分钟，带宽平均占用 1.3Mbps 左右。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240621/image.1sewj9gzr7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Docker 部署
在 Docker 部署和常规部署之间选择一个即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Linux/amd64&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;sudo docker image pull rustdesk/rustdesk-server
sudo docker run --name hbbs -p 21115:21115 -p 21116:21116 -p 21116:21116/udp -p 21118:21118 -v `pwd`:/root -td --net=host rustdesk/rustdesk-server hbbs -r &amp;lt;relay-server-ip[:port]&amp;gt;
sudo docker run --name hbbr -p 21117:21117 -p 21119:21119 -v `pwd`:/root -td --net=host rustdesk/rustdesk-server hbbr
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Linux/arm64v8&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;sudo docker image pull rustdesk/rustdesk-server:latest-arm64v8
sudo docker run --name hbbs -p 21115:21115 -p 21116:21116 -p 21116:21116/udp -p 21118:21118 -v `pwd`:/root -td --net=host rustdesk/rustdesk-server:latest-arm64v8 hbbs -r &amp;lt;relay-server-ip[:port]&amp;gt;
sudo docker run --name hbbr -p 21117:21117 -p 21119:21119 -v `pwd`:/root -td --net=host rustdesk/rustdesk-server:latest-arm64v8 hbbr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;据我所知，–net=host 仅适用于 Linux，它让 hbbs/hbbr 可以看到对方真实的 ip, 而不是固定的容器 ip (172.17.0.1)。 如果–net=host 运行正常，-p 选项就不起作用了, 可以去掉。&lt;/p&gt;
&lt;p&gt;请去掉 –net=host，如果您在非 Linux 系统上遇到无法连接的问题&lt;/p&gt;
&lt;h3&gt;1、常规部署 直接运行&lt;/h3&gt;
&lt;p&gt;This content is only supported in a ThunderSoft Docs&lt;/p&gt;
&lt;p&gt;在服务器上运行 hbbs/hbbr (Centos 或 Ubuntu)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./hbbs -r &amp;lt;hbbr运行所在主机的地址[:port]&amp;gt;
./hbbr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况下，hbbs 监听 21115(tcp), 21116(tcp/udp), 21118(tcp)，hbbr 监听 21117(tcp), 21119(tcp)。务必在防火墙开启这几个端口， 请注意 21116 同时要开启 TCP 和 UDP。其中 21115 是 hbbs 用作 NAT 类型测试，21116/UDP 是 hbbs 用作 ID 注册与心跳服务，21116/TCP 是 hbbs 用作 TCP 打洞与连接服务，21117 是 hbbr 用作中继服务, 21118 和 21119 是为了支持网页客户端。如果您不需要网页客户端（21118，21119）支持，对应端口可以不开。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TCP(21115, 21116, 21117, 21118, 21119)&lt;/li&gt;
&lt;li&gt;UDP(21116)
&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;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;21115&lt;/td&gt;
&lt;td&gt;tcp&lt;/td&gt;
&lt;td&gt;HBBS&lt;/td&gt;
&lt;td&gt;NAT 类型测试&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21116&lt;/td&gt;
&lt;td&gt;tcp/udp&lt;/td&gt;
&lt;td&gt;HBBS&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;打洞与连接服务/UDP ID 注册与心跳服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21117&lt;/td&gt;
&lt;td&gt;tcp&lt;/td&gt;
&lt;td&gt;HBBR&lt;/td&gt;
&lt;td&gt;中继服务&lt;/td&gt;
&lt;td&gt;HBBR 锚点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21118&lt;/td&gt;
&lt;td&gt;tcp&lt;/td&gt;
&lt;td&gt;HBBS&lt;/td&gt;
&lt;td&gt;WebSocket 服务&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21119&lt;/td&gt;
&lt;td&gt;tcp&lt;/td&gt;
&lt;td&gt;HBBR&lt;/td&gt;
&lt;td&gt;WebSocket 转发&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;如果你想选择自己的端口，使用 “-h” 选项查看帮助。&lt;/td&gt;
&lt;td&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;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2、守护进程&lt;/h2&gt;
&lt;p&gt;至此我们的自建服务器已经运行起来了，至此服务器完全可以使用 RustDesk，但你会发现当你退出了服务器远程连接，服务器就自动停掉了，所以我们还需要将自建服务器的进程守护起来。&lt;/p&gt;
&lt;p&gt;在 pm2 和 systemd 之间选择一个即可，建议使用 systemd。&lt;/p&gt;
&lt;h3&gt;1、使用 pm2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pm2 start hbbs -- -r &amp;lt;relay-server-ip[:port]&amp;gt;
pm2 start hbbr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pm2&lt;/code&gt; 需要 nodejs v16+，如果你运行 pm2 失败（例如在 &lt;code&gt;pm2 list&lt;/code&gt; 中看不到 hbbs/hbbr），请从 &lt;a href=&quot;https://nodejs.org/&quot;&gt;https://nodejs.org&lt;/a&gt; 下载并安装 LTS 版本的 nodejs。 如果你想让 hbbs/hbbr 在重启后自动运行，请查看 &lt;code&gt;pm2 save&lt;/code&gt; 和 &lt;code&gt;pm2 startup&lt;/code&gt;。 更多关于 pm2。另一个不错的日志工具是 &lt;a href=&quot;https://github.com/keymetrics/pm2-logrotate&quot;&gt;&lt;code&gt;pm2-logrotate&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;hhbs 的-r 参数不是必须的，他只是方便你不用在客户端指定中继服务器，如果是默认 21117 端口，可以不填 port。客户端指定的中继服务器优先级高于这个。&lt;/p&gt;
&lt;h4&gt;2、使用 systemd&lt;/h4&gt;
&lt;p&gt;官方建议使用 PM2 守护进程，如果你的主机没有其它使用 Node.js 工具的需求，只需要使用 systemd 来管理服务即可！&lt;/p&gt;
&lt;h4&gt;① HBBS&lt;/h4&gt;
&lt;p&gt;解压出来的 hbbs 文件先通过&lt;code&gt;chmod +x hbbs&lt;/code&gt;赋予可执行权限，先运行一次&lt;code&gt;./hbbs&lt;/code&gt;，生成用于客户端认证使用的公钥&lt;code&gt;id_ed25519.pub&lt;/code&gt;，随后用&lt;code&gt;cat id_ed25519.pub&lt;/code&gt;命令查看公钥并记下。然后通过喜欢的编辑器编辑&lt;code&gt;/etc/systemd/system/hbbs.service&lt;/code&gt;，将用于参考的以下配置根据需要进行修改并保存，这时也要将强制校验密钥以&lt;code&gt;-k _&lt;/code&gt;参数写入启动命令中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# systemd配置路径

# /etc/systemd/system/hbbs.service

[Unit]
Description=Rust Desk Service
After=network.target

[Service]
Type=simple
User=root
Restart=on-failure
RestartSec=5s
# 设置运行路径

WorkingDirectory=/*程序路径*/rustdesk

# 可修改锚点端口，当前为21116（锚点）和21115（锚点-1）和21118（锚点+2）

# -r用于指定网卡IP（适用多网卡），-k参数用于强制校验客户端公钥，用于避免未授权的使用

ExecStart=/*程序路径*/rustdesk/hbbs -r 0.0.0.0 -p 21116 -k _

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;② HBBR&lt;/h4&gt;
&lt;p&gt;解压出来的 hbbr 文件先通过&lt;code&gt;chmod +x hbbr&lt;/code&gt;赋予可执行权限，然后通过喜欢的编辑器编辑&lt;code&gt;/etc/systemd/system/hbbr.service&lt;/code&gt;，将用于参考的以下配置根据需要进行修改并保存，同样将密钥校验以&lt;code&gt;-k _&lt;/code&gt;参数写入启动命令中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# systemd配置路径

# /etc/systemd/system/hbbr.service

[Unit]
Description=Rust Desk Service
After=network.target

[Service]
Type=simple
User=root
Restart=on-failure
RestartSec=5s

# 设置运行路径

WorkingDirectory=/*程序路径*/rustdesk

# 可修改锚点端口，当前为21117（锚点）和21119（锚点+2）

# -k参数用于强制校验客户端公钥，用于避免未授权的使用

ExecStart=/*程序路径*/rustdesk/hbbr -p 21117 -k _

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;service&lt;/code&gt;设置好后，即可通过&lt;code&gt;service hbbs start&lt;/code&gt;和&lt;code&gt;service hbbr start&lt;/code&gt;来启动这两项服务，启动后可以通过&lt;code&gt;service hbbs status&lt;/code&gt;和&lt;code&gt;service hbbr status&lt;/code&gt;查看进程的运行状态，显示绿色的 Active 即无误。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240621/image.1758wz1g2b.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240621/image.6wql8jwu50.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一切准备就绪后即可通过&lt;code&gt;systemctl enable hbbs&lt;/code&gt;和&lt;code&gt;systemctl enable hbbr&lt;/code&gt;允许它们开机自启。&lt;/p&gt;
&lt;h3&gt;端口放行&lt;/h3&gt;
&lt;p&gt;在腾讯云服务器上，进入服务器详情-&amp;gt;防火墙-&amp;gt;添加规则，然后编辑相应端口和规则即可，不必关心以下指令。&lt;/p&gt;
&lt;p&gt;如果恰好你有能力搭建了一台物理主机，则需要你手动设置防火墙规则，&lt;code&gt;iptables&lt;/code&gt;、&lt;code&gt;firewalld&lt;/code&gt;、&lt;code&gt;ufw&lt;/code&gt;的命令分别如下，其中的端口请按照你的设置的进行放行（默认 21115-21117），这里需要注意 hbbs 锚点端口必须同时放行 tcp 和 udp。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# CentOS firewalld

firewall-cmd --zone=public --add-port=21115/tcp --permanent
firewall-cmd --zone=public --add-port=21116/tcp --permanent
firewall-cmd --zone=public --add-port=21116/udp --permanent
firewall-cmd --zone=public --add-port=21117/tcp --permanent

# Debian/Ubuntu ufw

ufw allow 21115/tcp
ufw allow 21116/tcp
ufw allow 21116/udp
ufw allow 21117/tcp

# iptables

iptables -I INPUT 1 -p tcp --dport 21115 -j ACCEPT
iptables -I INPUT 1 -p tcp --dport 21116 -j ACCEPT
iptables -I INPUT 1 -p udp --dport 21116 -j ACCEPT
iptables -I INPUT 1 -p tcp --dport 21117 -j ACCEPT
iptables-save //保存（解决重启失效）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端连接
&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240621/image.4ckqvwyfwe.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;ID 服务器填写你的服务器 IP 地址:21116&lt;/p&gt;
&lt;p&gt;中继服务器填写你的服务器 IP 地址:21117&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240621/image.7i08uutei9.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;随后你只需要使用 ID 和密码即可使用自建服务器的 RustDesk。&lt;/p&gt;
&lt;h3&gt;开机自启&lt;/h3&gt;
&lt;p&gt;在我们远程控制主机时，可能不得不完成重启的操作，远控软件不自启可不行，那么接下来我们一起来配置一下自启。&lt;/p&gt;
&lt;p&gt;编辑文本文档 rustdesk.service&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=RustDesk
Requires=network.target
After=systemd-user-sessions.service

[Service]
Type=simple
ExecStart=/usr/bin/rustdesk --service
PIDFile=/var/run/rustdesk.pid
KillMode=mixed
TimeoutStopSec=30
User=root
LimitNOFILE=100000

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;移动到 /usr/lib/systemd/system 文件夹下，执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable rustdesk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl status rustdesk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看服务运行状态：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240621/image.3go9ggqtjc.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考
【RustDesk】自建远程桌面服务替代 TeamViewer/Todesk – Luminous&apos; Home: &lt;a href=&quot;https://luotianyi.vc/6542.html&quot;&gt;https://luotianyi.vc/6542.html&lt;/a&gt;
安装 :: RustDesk 文档: &lt;a href=&quot;https://rustdesk.com/docs/zh-cn/self-host/install/&quot;&gt;https://rustdesk.com/docs/zh-cn/self-host/install&lt;/a&gt;
Working principle · Issue #594 · rustdesk/rustdesk: &lt;a href=&quot;https://github.com/rustdesk/rustdesk/issues/594#issuecomment-1138342668&quot;&gt;https://github.com/rustdesk/rustdesk/issues/594#issuecomment-1138342668&lt;/a&gt;
Systemd 入门教程：命令篇 - 阮一峰的网络日志: &lt;a href=&quot;https://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html&quot;&gt;https://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html&lt;/a&gt;
Systemd 入门教程：实战篇 - 阮一峰的网络日志: &lt;a href=&quot;https://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-part-two.html&quot;&gt;https://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-part-two.html&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>使用谷歌 Indexing API 进行推送</title><link>https://blog.wemang.com/posts/tool/%E4%BD%BF%E7%94%A8%E8%B0%B7%E6%AD%8Cindexingapi%E8%BF%9B%E8%A1%8C%E6%8E%A8%E9%80%81/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/%E4%BD%BF%E7%94%A8%E8%B0%B7%E6%AD%8Cindexingapi%E8%BF%9B%E8%A1%8C%E6%8E%A8%E9%80%81/</guid><pubDate>Thu, 06 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;此文章针对插件: &lt;a href=&quot;https://github.com/Stonewuu/halo-plugin-sitepush&quot;&gt;https://github.com/Stonewuu/halo-plugin-sitepush&lt;/a&gt; 的谷歌推送部分&lt;/p&gt;
&lt;p&gt;谷歌的推送需要很多配置, 有些麻烦, 并且网络环境得保证能访问谷歌&lt;/p&gt;
&lt;p&gt;这是官方教程: &lt;a href=&quot;https://developers.google.com/search/apis/indexing-api/v3/prereqs&quot;&gt;https://developers.google.com/search/apis/indexing-api/v3/prereqs&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;创建 Google API 项目&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;进入 &lt;a href=&quot;https://console.cloud.google.com/apis/credentials&quot;&gt;https://console.cloud.google.com/apis/credentials&lt;/a&gt; 新建一个项目&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.4jo41arscs.webp&quot; alt=&quot;Xnip2023-10-19_09-06-35&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;创建项目后再切换到新创建的项目进行下一步操作&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;创建服务账号&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;在左边栏点凭据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.1e8m2cypgb.webp&quot; alt=&quot;Xnip2023-10-19_09-15-36&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这里的服务帐号 ID 生成的邮箱在稍后会用到, ID 随意&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.b8wrh3n97.webp&quot; alt=&quot;Xnip2023-10-19_09-16-28&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;点击完成后完成创建&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;获取凭据文件&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.8hghhz5eik.webp&quot; alt=&quot;Xnip2023-10-19_09-19-45&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.39l6uzcwzy.webp&quot; alt=&quot;Xnip2023-10-19_09-20-11&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.361kx9kb5y.webp&quot; alt=&quot;Xnip2023-10-19_09-20-37&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.1hs802uenw.webp&quot; alt=&quot;Xnip2023-10-19_09-21-07&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这里创建了私钥之后会自动下载一个 xxx.json 文件, 这个就是凭据&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;开启 Indexing API 访问&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.7sn7xyjkj4.webp&quot; alt=&quot;Xnip2023-10-19_09-49-18&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;搜索 indexing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.2h8bd8xu6f.webp&quot; alt=&quot;Xnip2023-10-19_09-50-26&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;选择第一个启用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.2ver446k0z.webp&quot; alt=&quot;Xnip2023-10-19_09-50-44&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.5mntc6t2f7.webp&quot; alt=&quot;Xnip2023-10-19_09-51-12&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;添加用户权限&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;在 &lt;a href=&quot;https://search.google.com/search-console/&quot;&gt;https://search.google.com/search-console/&lt;/a&gt; 中选择自己已验证的资源&lt;/p&gt;
&lt;p&gt;进入设置添加用户权限&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.1ap04nad2i.webp&quot; alt=&quot;Xnip2023-10-19_09-43-22&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;把刚才生成的服务账号邮箱添加到此处, 并且权限选择拥有者 (一定要是拥有者)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241101/image.2rv56eeuho.webp&quot; alt=&quot;Xnip2023-10-19_09-44-06&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>ZeroTier使用自建Moon服务器加速</title><link>https://blog.wemang.com/posts/tool/zerotier%E4%BD%BF%E7%94%A8%E8%87%AA%E5%BB%BAmoon%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%8A%A0%E9%80%9F/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/zerotier%E4%BD%BF%E7%94%A8%E8%87%AA%E5%BB%BAmoon%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%8A%A0%E9%80%9F/</guid><pubDate>Tue, 07 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Foreword&lt;/h1&gt;
&lt;p&gt;最近老是出现奇怪的情况，家里的主机总是连不上，公司和笔记本都没问题，但是家里的经常出现开机以后要等很久很久，zerotier才能ping通，然后中间可能还会又ping不通了，只要ping不通基本rdp就连不上。&lt;/p&gt;
&lt;p&gt;怀疑可能是zerotier从国外打洞失败了，可能某个ip被墙了，但是平常看不出来，而ZeroTier可以自建服务器，加速p2p的访问，就想着试一试。&lt;/p&gt;
&lt;p&gt;看了一些评论说，有些运营商好像直接ban了zerotier的服务器，导致永久无法连接，但是如果使用moon中转的话，就没这个问题了。&lt;/p&gt;
&lt;h1&gt;Moon&lt;/h1&gt;
&lt;p&gt;一般Moon服务直接安装在国内的vps上就行了，只要自己能很轻松访问即可，走流量什么的都很少。&lt;/p&gt;
&lt;h1&gt;安装zerotier&lt;/h1&gt;
&lt;p&gt;安装zerotier&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s https://install.zerotier.com/ | sudo bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认是否安装成功&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@VM-12-13-centos ~]# zerotier-cli info
200 info eb444ec0d8 1.10.2 ONLINE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将vps加入到zerotier中，加入成功以后，去官网授权&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zerotier-cli join 你创建的网络分配的id
200 join OK
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;创建moon&lt;/h1&gt;
&lt;p&gt;这里创建moon配置，并且修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod 777 /var/lib/zerotier-one
cd /var/lib/zerotier-one
sudo zerotier-idtool initmoon identity.public &amp;gt; moon.json
vi moon.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里面需要修改stableEndpoints，将你的提供moon服务的ip和端口加入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
 &quot;id&quot;: &quot;eb444ec0d8&quot;,
 &quot;objtype&quot;: &quot;world&quot;,
 &quot;roots&quot;: [
  {
   &quot;identity&quot;: &quot;eb444ec0d8:0:55f55633d3234c76d69dfe19742c6e002f2dada7e03f1a6e756353ed5c933171c8543a2a1796134c034a5c189def154cd8d465f05298d16c358746f2c2075ece&quot;,
   &quot;stableEndpoints&quot;: [&quot;你vps的ip/9993&quot;]
  }
 ],
 &quot;signingKey&quot;: &quot;f0c4f0b645f480be69054f6eb04fbba53d1b61c617cbf489df544956a5c0fa39a16b13882099f98a91011e817da2b1c8d15032d300f10e2d4629e333fa4459c8&quot;,
 &quot;signingKey_SECRET&quot;: &quot;9af8bb728b9aaa258108dadc203abeccb82bd563411054c78196da3aa02e6dd6d3535407d7ae3363734628f65b968db98220933d978d47906d0f9403eb720361&quot;,
 &quot;updatesMustBeSignedBy&quot;: &quot;f0c4f0b645f480be69054f6eb04fbba53d1b61c617cbf489df544956a5c0fa39a16b13882099f98a91011e817da2b1c8d15032d300f10e2d4629e333fa4459c8&quot;,
 &quot;worldType&quot;: &quot;moon&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意如果vps有防火墙、安全组之类的，9993的端口也要对应给放行，否则可能不成功&lt;/p&gt;
&lt;p&gt;这里就是根据配置生成对应的签名配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo zerotier-idtool genmoon moon.json
wrote 000000eb444ec0d8.moon (signed world with timestamp 1675475060380)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将配置文件放到moons.d的文件夹中，每个人配置文件名可能不同&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir moons.d
mv 000000eb444ec0d8.moon moons.d/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启zerotier&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart zerotier-one.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试
Windows客户端测试，首先查看是否能找到中转节点，windows cmd可能需要管理员权限打开&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zerotier-cli.bat listpeers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241029/image.3uuud2xrwm.png&quot; alt=&quot;&quot; /&gt;
一般这里会看到一个你的moon所在的vps的地址，那个的ztaddr就是我们要找的，比如我这里最后一条就是我实际的vps地址&lt;/p&gt;
&lt;p&gt;指定使用中转节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zerotier-cli.bat orbit ztaddr ztaddr
zerotier-cli.bat orbit 你的ztaddr 你的ztaddr
200 orbit OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认是否加入，可以看到他又之前的LEAF变成了MOON，就是正确加入了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241029/image.lvqgfbm9c.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用之前我是根本连不上家里的主机的，使用之后，直接就可以ping了&lt;/p&gt;
&lt;p&gt;ping检测，可以看到我的ping很低了，平常可以ping时会跳80-90甚至非常高，不是很稳定。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20241029/image.6t74gl80ma.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;ping的表现这么好以后，后续使用moonlight等其他服务的时候也变得异常丝滑&lt;/p&gt;
&lt;h1&gt;Summary&lt;/h1&gt;
&lt;p&gt;早知道就早点搭建了，简直不要太好用了&lt;/p&gt;
&lt;p&gt;如果有各种zerotier连不上的，我估计也可以直接通过中转加速来实现打洞。&lt;/p&gt;
&lt;h1&gt;Quote&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;https://www.cnblogs.com/NanKe-Studying/p/16343774.html&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;https://post.smzdm.com/p/a7nxv3ql/&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Hugo博客引入Giscus评论系统</title><link>https://blog.wemang.com/posts/tool/hugo%E5%8D%9A%E5%AE%A2%E5%BC%95%E5%85%A5giscus%E8%AF%84%E8%AE%BA%E7%B3%BB%E7%BB%9F/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/hugo%E5%8D%9A%E5%AE%A2%E5%BC%95%E5%85%A5giscus%E8%AF%84%E8%AE%BA%E7%B3%BB%E7%BB%9F/</guid><description>Hugo博客引入Giscus评论系统</description><pubDate>Sat, 04 Oct 2025 02:04:12 GMT</pubDate><content:encoded>&lt;p&gt;Hugo博客引入Giscus评论系统&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h1&gt;1. 选择一个评论系统&lt;/h1&gt;
&lt;p&gt;当前使用的 hugo 生成静态博客，主题是 LoveIt。该主题已经支持了多种评论系统：&lt;/p&gt;
&lt;p&gt;disqus
gitalk
valine
facebook
telegram
giscus
….
有很多，其中 giscus 是基于 Github Discussions 的，我们的博客也是放在 Github 的，那么为了简单起见就选择 giscus 吧。&lt;/p&gt;
&lt;h2&gt;什么是 gitcus？&lt;/h2&gt;
&lt;p&gt;Giscus 是由 GitHub Discussions 驱动的评论系统，根据官网，Giscus的特性有：&lt;/p&gt;
&lt;p&gt;开源。
无跟踪，无广告，永久免费。
无需数据库。全部数据均储存在 GitHub Discussions 中。
支持自定义主题！
支持多种语言。
高度可配置。
自动从 GitHub 拉取新评论与编辑。
可自建服务！&lt;/p&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;Giscus 使用 GitHub Discussions 作为数据库存储博客下面的评论。&lt;/p&gt;
&lt;p&gt;Giscus 插件加载时，会使用 GitHub Discussions 搜索 API 根据选定的映射方式（如 URL、pathname、 等）来查找与当前页面关联的 discussion。如果找不到匹配的 discussion，giscus bot 就会在第一次有人留下评论或回应时自动创建一个 discussion。&lt;/p&gt;
&lt;p&gt;要评论，访客必须按 GitHub OAuth 流程授权 giscus app 代表他发帖。或者访客也可以直接在 GitHub Discussion 里评论，作者可以在 GitHub 上管理评论。&lt;/p&gt;
&lt;h1&gt;2. 引入 giscus&lt;/h1&gt;
&lt;p&gt;由于主题已经支持 gitcus 评论了，因此引入其实是比较简单的，大致分为以下几个步骤：&lt;/p&gt;
&lt;p&gt;0）选择一个仓库作为存储 Discussions 的仓库&lt;/p&gt;
&lt;p&gt;一般选择博客本身即可，比如这里我用的就是这个 zhouzongyan/zhouzongyan.github.io&lt;/p&gt;
&lt;h2&gt;1）安装 giscus&lt;/h2&gt;
&lt;p&gt;将 giscus 安装到上一步指定的仓库，这样 giscus 才有权限获取数据&lt;/p&gt;
&lt;h2&gt;2）开启 GitHub Discussions&lt;/h2&gt;
&lt;p&gt;将上面选择的仓库开启 GitHub Discussions 用于存放评论&lt;/p&gt;
&lt;h2&gt;3）从 giscus 官网获取配置信息&lt;/h2&gt;
&lt;h2&gt;4）将上一步中获取的配置添加到博客配置&lt;/h2&gt;
&lt;h3&gt;1. 选择一个仓库&lt;/h3&gt;
&lt;p&gt;一般选择博客本身即可，比如这里我用的就是这个 zhouzongyan/zhouzongyan.github.io&lt;/p&gt;
&lt;h3&gt;2. 安装 giscus&lt;/h3&gt;
&lt;p&gt;点击 这里 进入 giscus app 的 安装界面，大概长这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/image.45g6wa64v.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击安装，会提示选择一个仓库，这里就选择上一步中指定的仓库即可，后续 giscus 就会从该仓库读取数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.4xub31l1nh.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3. 开启 Discussions&lt;/h3&gt;
&lt;p&gt;然后打开之前选择的仓库，进入设置界面，勾选上 Discussions 以开启该仓库的 Discussions。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.39ky5uw5pn.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;4. 从 giscus 官网获取配置信息&lt;/h3&gt;
&lt;p&gt;完成上述准备工作后就可以访问 giscus 官网 获取配置信息了，具体如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.7ljrdefy4b.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;几个标箭头的地方需要注意：&lt;/p&gt;
&lt;p&gt;1）填写前面选择的仓库
正常情况下填写后会提示满足条件，如果提示不满足条件就检查下前面几个步骤是不是有遗漏
2）页面和 discussion 的映射关系
这里一般用默认的 pathname 即可
该选项主要会影响创建的 discussion 的名字
3）最后就是选择后面 giscus 创建的 discussion 的分类
一般选择 Announcements，因为 Announcements 类型的 discussion 只有管理员才有权限操作，这样便于管理
上面几个地方配置完成后，页面往下滑，会生成一个配置文件：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.sypqxq7wk.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个就是我们后续要用到的配置文件，主要关注以下字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;data-repo&lt;/li&gt;
&lt;li&gt;data-repo-id&lt;/li&gt;
&lt;li&gt;data-category&lt;/li&gt;
&lt;li&gt;data-category-id&lt;/li&gt;
&lt;li&gt;data-mapping&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 配置到博客&lt;/h3&gt;
&lt;p&gt;将上面获取到的配置信息添加博客主题对应的位置即可。&lt;/p&gt;
&lt;p&gt;这里不同的主题配置方式可能不太一样，不过需要配置的即可参数应该是相同的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[params.page.comment]
enable = true
[params.page.comment.giscus]
# You can refer to the official documentation of giscus to use the following configuration.
# 你可以参考官方文档来使用下列配置
enable = true
repo = &quot;lixd/lixd.github.io&quot;
repoId = &quot;xxx&quot;
category = &quot;Announcements&quot;
categoryId = &quot;xxxx&quot;
mapping = &quot;pathname&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置好之后重新部署即可看到效果。&lt;/p&gt;
&lt;h1&gt;3. 效果展示&lt;/h1&gt;
&lt;h2&gt;预览&lt;/h2&gt;
&lt;p&gt;点开一篇文章，滑到底部会看到评论区，大概长这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.3d4k3kxvgt.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过 github 登录后即可参与评论。&lt;/p&gt;
&lt;p&gt;discussion
有人评论后，giscus 会自动在配置好的仓库的 discussion 里创建一条数据用于保存评论。&lt;/p&gt;
&lt;p&gt;大概长这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.6t6vvo8146.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这里可以对评论进行管理~&lt;/p&gt;
&lt;p&gt;到此，整个配置就完成了，具体细节可以参考 &lt;a href=&quot;https://github.com/zhouzongyan/zhouzongyan.github.io&quot;&gt;这个仓库&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Giscus的基础设置</title><link>https://blog.wemang.com/posts/tool/giscus%E7%9A%84%E5%9F%BA%E7%A1%80%E8%AE%BE%E7%BD%AE/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/giscus%E7%9A%84%E5%9F%BA%E7%A1%80%E8%AE%BE%E7%BD%AE/</guid><description>Giscus的基础设置</description><pubDate>Sat, 04 Oct 2025 01:47:42 GMT</pubDate><content:encoded>&lt;p&gt;不过这两项评论系统与我目前搭建博客的理念是相悖的。一来是这两个系统没有让我获得所有的评论数据，二来是这两个系统让评论者也默认了他们的评论数据被交由这两个公司处理。随后了解到一些Gitment、Gitalk等一些基于GitHub Issues设计的评论系统。显然这些基于GitHub Issues的评论系统更符合我现在的理念。最后刚好也是因为目前这个博客主题Chirpy的更新，我也决定加入一个评论系统到博客里面。毕竟没有评论系统的博客感觉更像是一个对于作者本人的笔记本，少了一些可以讨论的部分。&lt;/p&gt;
&lt;p&gt;新的Chirpy 5.0主题内置了三个评论系统的支持，分别是Disqus、Utterance和Giscus。这里选择了最年轻的Giscus作为评论系统。相关的配置在_config.yml下可以看到如下的配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  giscus:
    repo:             # &amp;lt;gh-username&amp;gt;/&amp;lt;repo&amp;gt;
    repo_id:
    category:
    category_id:
    mapping:          # optional, default to &apos;pathname&apos;
    input_position:   # optional, default to &apos;bottom&apos;
    lang:             # optional, default to the value of `site.lang`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很显然其中三项并没有给出太详细的信息，在查询了多个帖子后，我确定了三项的对应的数据。在这里记录一下查询的方法。&lt;/p&gt;
&lt;p&gt;首先，repo_id是托管博客的代码仓库的一个标识值，category是该仓库Issues里面对应的分类（或者说是主题）。一个仓库默认具有下面几个分类：Announcements、General、Ideas、Q&amp;amp;A、Show and tell。这里我选择General作为评论的分类。最后的category_id类似repo_id也是对该分类的一个标识值。&lt;/p&gt;
&lt;p&gt;如何快速的获取这些数据呢，可以通过GitHub官方的&lt;a href=&quot;https://docs.github.com/en/graphql/overview/explorer&quot;&gt;GraphQL API Explorer&lt;/a&gt;查询到。这里把查询所用的语句进行记录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  repository(owner:  &quot;MichaelTan9999&quot;, name: &quot;michaeltan9999.github.io&quot;) {
    id
    discussionCategories (first: 5) {
      nodes {
        name
        id
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;点击查询后，我们会得到如下结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;repository&quot;: {
      &quot;id&quot;: &quot;R_kgDOG6UKFQ&quot;,
      &quot;discussionCategories&quot;: {
        &quot;nodes&quot;: [
          {
            &quot;name&quot;: &quot;Announcements&quot;,
            &quot;id&quot;: &quot;DIC_kwDOG6UKFc4CBXaW&quot;
          },
          {
            &quot;name&quot;: &quot;General&quot;,
            &quot;id&quot;: &quot;DIC_kwDOG6UKFc4CBXaX&quot;
          },
          {
            &quot;name&quot;: &quot;Ideas&quot;,
            &quot;id&quot;: &quot;DIC_kwDOG6UKFc4CBXaZ&quot;
          },
          {
            &quot;name&quot;: &quot;Q&amp;amp;A&quot;,
            &quot;id&quot;: &quot;DIC_kwDOG6UKFc4CBXaY&quot;
          },
          {
            &quot;name&quot;: &quot;Show and tell&quot;,
            &quot;id&quot;: &quot;DIC_kwDOG6UKFc4CBXaa&quot;
          }
        ]
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将对应的ID填到_config.yml就大功告成了。&lt;/p&gt;
&lt;p&gt;别忘了在GitHub个人设置里面的Integrations中安装Giscus并授权&lt;/p&gt;
</content:encoded></item><item><title>一台电脑上使用SSH同时连接多个Git账户</title><link>https://blog.wemang.com/posts/tool/%E4%B8%80%E5%8F%B0%E7%94%B5%E8%84%91%E4%B8%8A%E4%BD%BF%E7%94%A8ssh%E5%90%8C%E6%97%B6%E8%BF%9E%E6%8E%A5%E5%A4%9A%E4%B8%AAgit%E8%B4%A6%E6%88%B7/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/%E4%B8%80%E5%8F%B0%E7%94%B5%E8%84%91%E4%B8%8A%E4%BD%BF%E7%94%A8ssh%E5%90%8C%E6%97%B6%E8%BF%9E%E6%8E%A5%E5%A4%9A%E4%B8%AAgit%E8%B4%A6%E6%88%B7/</guid><pubDate>Fri, 03 Oct 2025 19:10:00 GMT</pubDate><content:encoded>&lt;ol&gt;
&lt;li&gt;同时两个Github账户
1.1. 建立SSH公私钥
先打开Git Bash窗口，输入命令，切换到对应目录C:\Users\随心.ssh，随心是你电脑对应的用户名&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cd ~/.ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.8ojjpcmnoz.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;执行命令，生成第一个账号的SSH&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t rsa -C &quot;&amp;lt;1107224733@qq.com&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不要一路回车，在第一个对话的时候输入公私钥重命名为id_rsa_dolyw&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/image.1lbo9qod29.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同理第二个也是这样&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t rsa -C &quot;&amp;lt;158020951@qq.com&amp;gt;&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在第一个对话的时候输入公私钥重命名为id_rsa_wliduo&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/image.4jnyd8y4k6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里使用两个账号，以后只需按照这样继续添加账号即可，切换到对应目录C:\Users\随心.ssh，一个账号两个文件(私钥和公钥)&lt;/p&gt;
&lt;p&gt;Ps: 好像如果是Linux还需要添加一下私钥文件，Windows好像不用&lt;/p&gt;
&lt;p&gt;1.2. 建立配置文件
输入下面命令建立config文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开文件输入下面代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host dolyw
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_dolyw

Host wliduo
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_wliduo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.1e8geb567e.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.1hs2c0yndo.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1.3.登录配置公钥
登录Github配置公钥，登录点击Settings，SSH and GPG keys，New SSH key
打开对应账号的id_rsa_dolyw.pub公钥文件，把内容复制到Key里&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.73tspw0fse.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.86ti0rwoej.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加成功，测试是否成功，打开窗口，输入命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -T dolyw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.7sn29wp8b7.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样就成功了，wliduo账户一样的操作，登录Github配置公钥&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.361f97r2vs.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.45himedm3i.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -T wliduo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.6ik53l8ss5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;成功，可以使用两个账号Clone，Push测试下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;添加码云(Gitee)账户
2.1. 建立SSH公私钥
先打开Git Bash窗口，输入命令，切换到对应目录C:\Users\随心.ssh，随心是你电脑对应的用户名&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cd ~/.ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行命令，生成第一个账号的SSH&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t rsa -C &quot;&amp;lt;1107224733@qq.com&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不要一路回车，在第一个对话的时候输入公私钥重命名为id_rsa_gitee&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.9gwf73huyg.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2.2. 修改配置文件
打开C:\Users\随心.ssh\config文件输入下面代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host gitee
HostName gitee.com
User git
IdentityFile ~/.ssh/id_rsa_gitee
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.7egmj1jvxf.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2.3. 登录配置公钥
去码云登录账号添加部署SSH公钥&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.7egmj1k9fs.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加成功，测试是否成功，打开窗口，输入命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -T gitee
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.2dojrhdyo6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样就成功了&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;添加Coding账户
3.1. 建立SSH公私钥
先打开Git Bash窗口，输入命令，切换到对应目录C:\Users\随心.ssh，随心是你电脑对应的用户名&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cd ~/.ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行命令，生成第一个账号的SSH&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t rsa -C &quot;&amp;lt;1107224733@qq.com&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不要一路回车，在第一个对话的时候输入公私钥重命名为id_rsa_coding&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.7p55pmz1a.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;3.2. 修改配置文件
打开C:\Users\随心.ssh\config文件输入下面代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host coding

# HostName git.dev.tencent.com

HostName e.coding.net
User git
IdentityFile ~/.ssh/id_rsa_coding
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.86ti0s2myj.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;3.3. 登录配置公钥
去Coding登录账号添加部署SSH公钥&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.8s35n2xggu.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加成功，测试是否成功，打开窗口，输入命令，输入yes确定&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -T coding
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.4uas6en47b.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样就成功了&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;地址使用注意
地址必须注意类似&lt;code&gt;&amp;lt;git@github.com&amp;gt;:dolyw/ShiroJwt.git，github.com&lt;/code&gt;需要更换为配置中的host
例如&lt;code&gt;&amp;lt;git@github.com&amp;gt;:dolyw/ShiroJwt.git&lt;/code&gt;需要修改为&lt;code&gt;git@dolyw:dolyw/ShiroJwt.git&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.39l16xqddg.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.67xbafyyey.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;账户使用注意
默认是全局的用户名和邮箱，如果不想使用全局的用户名和邮箱，记得给每个仓库设置局部的用户名和邮箱&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;git config user.name &quot;name&quot;
git config user.email &quot;email&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Gitlab使用注意
生成密钥看文档先，注意 -b 4096&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t rsa -C &quot;&amp;lt;your.email@example.com&amp;gt;&quot; -b 4096
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;工具使用注意
7.1. IDEA
IDEA记得设置SSH认证为Native&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.9rj9092gjr.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;7.2. TortoiseGit
TortoiseGit记得设置SSH认证为Git默认的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.2obdkmxcyk.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;修改Https为SSH
也可以直接修改.git下的config文件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240611/image.86ti0s5xbw.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED
Git 错误 错误描述 错误原因 解决办法 错误描述 错误原因 远程的 Gitlab 服务发送了变化，加密的也随之发生了变化，所以本地 /.ssh/known_hosts 中保存的也发生了变化。 解决办法 (1)删除 /.ssh/known_hosts 文件中的内容 (2)重新 pull 一下即可&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Docker 常用命令大全</title><link>https://blog.wemang.com/posts/tool/docker%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4%E5%A4%A7%E5%85%A8/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/docker%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4%E5%A4%A7%E5%85%A8/</guid><pubDate>Fri, 03 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Docker 常用命令导图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/github.com/zhouzongyan/picx-images-hosting/raw/master/20240619/image.2obdvmc3d9.webp&quot; alt=&quot;Docker 常用命令导图&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;基本命令&lt;/h1&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;docker info&lt;/td&gt;
&lt;td&gt;检查当前容器的安装情况（包括镜像数、容器书、多少个物理机节点）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker version&lt;/td&gt;
&lt;td&gt;查看当前安装的 Docker 的版本信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;容器生命周期相关命令&lt;/h1&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;docker run -d -p x:x --name xxx 镜像id&lt;/td&gt;
&lt;td&gt;以后台方式运行容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker create --name xxx nginx:latest&lt;/td&gt;
&lt;td&gt;创建一个新的容器，但是不启动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker start/stop/restart&lt;/td&gt;
&lt;td&gt;启动\停止\重启一个容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker kill&lt;/td&gt;
&lt;td&gt;容器id 终止一个运行中的容器，kill 不管是否同意，直接强制终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker rm -vf&lt;/td&gt;
&lt;td&gt;容器id 删除一个或者多个容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker exec -it id bash&lt;/td&gt;
&lt;td&gt;进入到容器内部&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker attach&lt;/td&gt;
&lt;td&gt;进入到容器内部&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;容器操作相关命令&lt;/h1&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;docker ps -a grep xxxx&lt;/td&gt;
&lt;td&gt;显示某一个组件 XXX 的容器列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker inspect id&lt;/td&gt;
&lt;td&gt;获取容器或者镜像的元数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker top id&lt;/td&gt;
&lt;td&gt;查看容器中运行的进程信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker stats id&lt;/td&gt;
&lt;td&gt;实时显示容器资源的使用统计&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker events&lt;/td&gt;
&lt;td&gt;从服务器获取实时事件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker logs id&lt;/td&gt;
&lt;td&gt;查看容器内的标准日志输出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker port id&lt;/td&gt;
&lt;td&gt;列出指定容器的端口映射&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker cp ./t.txt id:/root/&lt;/td&gt;
&lt;td&gt;将宿主机当前目录下的 t.txt 复制到 id 容器中的 root 目录下&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker diff id&lt;/td&gt;
&lt;td&gt;列出该容器自创建以来，容器内部文件的变化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker commit -m &quot;comment&quot; -a &quot;authon&quot;&lt;/td&gt;
&lt;td&gt;容器id repository:tag 将指定容器打包成一个本地镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker update --memory=16G&lt;/td&gt;
&lt;td&gt;修改容器运行中的配置，即时生效无需配置&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;本地镜像管理相关命令&lt;/h1&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;docker images&lt;/td&gt;
&lt;td&gt;列出本地宿主机上的所有镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker history id&lt;/td&gt;
&lt;td&gt;查看指定镜像的分层结构以及创建历史&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker image inspect id&lt;/td&gt;
&lt;td&gt;查看镜像的元数据信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker rmi id&lt;/td&gt;
&lt;td&gt;根据镜像 id 删除镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker tag image-name:tag&lt;/td&gt;
&lt;td&gt;给指定镜像增加 tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ocekr build -t tag .&lt;/td&gt;
&lt;td&gt;通过当前目目录下的 Dockerfile 来构建一个标签为 tag 的镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker export -o xxx.tar id&lt;/td&gt;
&lt;td&gt;将镜像打包成文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker import xxx.tar name&lt;/td&gt;
&lt;td&gt;从归档文件中创建镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker save -o xxx.tat id&lt;/td&gt;
&lt;td&gt;将指定镜像保存为归档文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker load --input xxx.tar&lt;/td&gt;
&lt;td&gt;用于将 docker save 生成的归档文件还原成镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;镜像仓库相关命令&lt;/h1&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;docker loging -u xxx -p xxx&lt;/td&gt;
&lt;td&gt;登录一个 docker 镜像仓库，如果未指定镜像仓库地址，则默认为Docker Hub镜像仓库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker logout&lt;/td&gt;
&lt;td&gt;退出登录的镜像仓库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker pull nginx&lt;/td&gt;
&lt;td&gt;从默认的 Docker hub 上拉取 nginx 镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker push image_name&lt;/td&gt;
&lt;td&gt;将本地镜像上传到镜像仓库（注意需要先登录）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker search xxx&lt;/td&gt;
&lt;td&gt;从默认的 Docker Hub 中搜索指定的镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;参数命令&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;以下汇总 docker run 命令支持的相关参数：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;容器运行相关参数&lt;/h2&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;-a,--attach=[]&lt;/td&gt;
&lt;td&gt;是否绑定到标准输入、输出、和错误.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-d,--detach=true&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--detach-keys=&quot;&quot;&lt;/td&gt;
&lt;td&gt;从attach模式退出的快捷键，默认是 Ctrl+P&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--entrypoint=&quot;&quot;&lt;/td&gt;
&lt;td&gt;镜像存在入口命令时覆盖新的命令.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-expost=[]&lt;/td&gt;
&lt;td&gt;暴露出来的镜像端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--group-add=[]&lt;/td&gt;
&lt;td&gt;运行容器的用户组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-i&lt;/td&gt;
&lt;td&gt;保持标准输入打开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--ipc=&quot;&quot;&lt;/td&gt;
&lt;td&gt;容器 IP 命名空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--isolation=&quot;default&quot;&lt;/td&gt;
&lt;td&gt;容器使用的隔离机制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--log-driver=&quot;&quot;&lt;/td&gt;
&lt;td&gt;指定容器的日志驱动类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--log-opy=[]&lt;/td&gt;
&lt;td&gt;传递给日志驱动的选项&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--net=&quot;bridge&quot;&lt;/td&gt;
&lt;td&gt;指定容器的网络模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--net-alias=[]&lt;/td&gt;
&lt;td&gt;容器在网络中的别名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--volume=[=[HOST-DIR]:]CONTAINER-DIR:[:OPTIONS]&lt;/td&gt;
&lt;td&gt;挂载宿主机上的数据卷到容器内&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--volume-driver=&quot;&quot;&lt;/td&gt;
&lt;td&gt;挂载数据卷的驱动类型.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--volumes-from=[]&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;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;--add-host=[]&lt;/td&gt;
&lt;td&gt;在容器内添加一个宿主机名到 IP 地址的映射关系，修改 HOST 文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--device=[]&lt;/td&gt;
&lt;td&gt;映射物理机上的设备到容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--dns-search=[]&lt;/td&gt;
&lt;td&gt;指定容器 DNS 搜索域&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--dns-opt=[]&lt;/td&gt;
&lt;td&gt;自定义的 DNS 选项&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--dns=[]&lt;/td&gt;
&lt;td&gt;自定义的 DNS 服务器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-e,-env=[]&lt;/td&gt;
&lt;td&gt;指定容器内的环境变量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-env-file=[]&lt;/td&gt;
&lt;td&gt;从文件中读取环境变量到容器内&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-h,--hostname=&quot;&quot;&lt;/td&gt;
&lt;td&gt;指定容器中的主机名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--ip=&quot;&quot;&lt;/td&gt;
&lt;td&gt;指定容器内 ip4 的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--ip6=&quot;&quot;&lt;/td&gt;
&lt;td&gt;指定容器内 ip6 的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--link=[id]&lt;/td&gt;
&lt;td&gt;连接到其他容器，不需要其他容器暴露端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--name==&quot;&quot;&lt;/td&gt;
&lt;td&gt;指定容器的别名&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;CPU 相关参数&lt;/h1&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;--cpu-shares=0&lt;/td&gt;
&lt;td&gt;允许容器使用 CPU 资源的相对权重，默认一个容器能用满一个 CPU，用于设置多个容器竞争 CPU 时，各个容器相对能分配到 CPU 时间占比&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--cpu-period=0&lt;/td&gt;
&lt;td&gt;限制容器在 CFS 调度下 CPU 占用的时间片，用于绝对设置容器能使用的 CPU 时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--cpu-quota=0&lt;/td&gt;
&lt;td&gt;限制容器在 CFS 调度器下的 CPU 配额，用于绝对设置容器能使用 CPU 的时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--cpuset-cpus=&quot;&quot;&lt;/td&gt;
&lt;td&gt;限制容器能使用那些 cpu 核数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--cpuset-mems=&quot;&quot;&lt;/td&gt;
&lt;td&gt;NUMA 架构下使用那些核心内存&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;内存资源相关参数&lt;/h1&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;--kernel-memory=&quot;&quot;&lt;/td&gt;
&lt;td&gt;限制容器使用内核的内存大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-m,--memory=&quot;&quot;&lt;/td&gt;
&lt;td&gt;限制容器内应用使用内存的大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--memory-reservation=&quot;&quot;&lt;/td&gt;
&lt;td&gt;当系统中内存过小时，容器会被强制限制内存到给定值默认情况下等于内存限制值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--memory-swap=&quot;LIMIT&quot;&lt;/td&gt;
&lt;td&gt;限制容器使用内存和交换分区的大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--oom-kill-disable=true&lt;/td&gt;
&lt;td&gt;内存耗尽时是否终止容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--oom-score-adj=&quot;&quot;&lt;/td&gt;
&lt;td&gt;调整容器的内存耗尽参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--memory-swappiness=&quot;0-100&quot;&lt;/td&gt;
&lt;td&gt;调整容器的内存交换分区参数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;容器磁盘 IO 控制相关参数&lt;/h1&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;--device-read-bps&lt;/td&gt;
&lt;td&gt;限制此设备上的读速率&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--device-read-iops&lt;/td&gt;
&lt;td&gt;通过每秒读 IO 次数限制指定设备的读速率&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--device-write-bps&lt;/td&gt;
&lt;td&gt;限制此设备的写速率&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--device-write-iops&lt;/td&gt;
&lt;td&gt;通过每秒写 IO 次数限制指定设备的写速率&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--blkio-weight&lt;/td&gt;
&lt;td&gt;容器默认磁盘 IO 的加权值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--blkio-weight-device&lt;/td&gt;
&lt;td&gt;针对特定设备的 IO 加权控制&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>Node.js 常见问题</title><link>https://blog.wemang.com/posts/node/faq/error/</link><guid isPermaLink="true">https://blog.wemang.com/posts/node/faq/error/</guid><pubDate>Wed, 24 Sep 2025 18:10:00 GMT</pubDate><content:encoded>&lt;h2&gt;error:0308010C:digital envelope routines::unsupported&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;node:internal/crypto/hash:67
  this[kHandle] = new _Hash(algorithm, xofLen);
                  ^

Error: error:0308010C:digital envelope routines::unsupported
    at new Hash (node:internal/crypto/hash:67:19)
    at Object.createHash (node:crypto:133:10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip 解决方案&lt;/p&gt;
&lt;h3&gt;错误原因&lt;/h3&gt;
&lt;p&gt;在 Node.js v17 中，Node.js 开发人员关闭了 SSL 提供程序中的一个安全漏洞。此修复是一项重大更改，与 NPM 中 SSL 包中的类似重大更改相对应。当您尝试在 Node.js v17 或更高版本中使用 SSL 而不升​​级 package.json 中的那些 SSL 包时，您将看到此错误。&lt;/p&gt;
&lt;h4&gt;危险的解决方案&lt;/h4&gt;
&lt;p&gt;将 Node.js 版本从 17+ 降级到 16+，或者使用旧的 SSL 提供程序。但这两种解决方案都是使您的构建容易受到安全威胁的黑客攻击。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Linux and macOS
export NODE_OPTIONS=--openssl-legacy-provider

# Windows
set NODE_OPTIONS=--openssl-legacy-provider

# Windows PowerShell
$env:NODE_OPTIONS = &quot;--openssl-legacy-provider&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;正确（安全）的解决方案&lt;/h4&gt;
&lt;p&gt;使用最新版本的 Node.js，并使用最新的带有安全修复程序的包。以下命令将解决此问题（注意：对于复杂的应用，此命令会引入可能破坏原程序构建的安全修复）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm audit fix --force
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;参考资料&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/69692842/error-message-error0308010cdigital-envelope-routinesunsupported&quot;&gt;Error message &quot;error:0308010C:digital envelope routines::unsupported&quot;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>从Hexo到Astro博客1分钟迁移指南</title><link>https://blog.wemang.com/posts/%E4%BB%8Ehexo%E5%88%B0astro%E5%8D%9A%E5%AE%A21%E5%88%86%E9%92%9F%E8%BF%81%E7%A7%BB%E6%8C%87%E5%8D%97/</link><guid isPermaLink="true">https://blog.wemang.com/posts/%E4%BB%8Ehexo%E5%88%B0astro%E5%8D%9A%E5%AE%A21%E5%88%86%E9%92%9F%E8%BF%81%E7%A7%BB%E6%8C%87%E5%8D%97/</guid><pubDate>Mon, 07 Apr 2025 17:20:46 GMT</pubDate><content:encoded>&lt;p&gt;:::note{type=&quot;info&quot;}
🎉 本文将介绍如何将 Hexo 博客迁移到 Astro 博客，只需要 1 分钟即可完成！&lt;/p&gt;
&lt;p&gt;作为 &lt;code&gt;Hexo&lt;/code&gt; 的长期用户，我在 2024 年开始尝试 &lt;code&gt;Astro&lt;/code&gt;，&lt;code&gt;Hexo&lt;/code&gt; 基于模板引擎（EJS/Swig）的字符串拼接，而 &lt;code&gt;Astro&lt;/code&gt; 采用基于 &lt;code&gt;Vite&lt;/code&gt; 的组件化构建体系。&lt;code&gt;Hexo&lt;/code&gt; 在 &lt;code&gt;hexo generate&lt;/code&gt; 时完成全量渲染，&lt;code&gt;Astro&lt;/code&gt; 在 &lt;code&gt;astro build&lt;/code&gt; 时执行 &lt;code&gt;SSG&lt;/code&gt;（静态站点生成）+ &lt;code&gt;ISR&lt;/code&gt;（增量静态再生）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Astro&lt;/code&gt; 的组件化架构、按需加载，构建速度、&lt;code&gt;Vite&lt;/code&gt; 驱动，热更新速度快等现代化开发体验 深深吸引了我！
:::&lt;/p&gt;
&lt;h3&gt;项目结构对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hexo&lt;/th&gt;
&lt;th&gt;Astro&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;_config.yml&lt;/td&gt;
&lt;td&gt;src/config.ts&lt;/td&gt;
&lt;td&gt;配置文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;source&lt;/td&gt;
&lt;td&gt;src/content/blog&lt;/td&gt;
&lt;td&gt;主内容目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;themes&lt;/td&gt;
&lt;td&gt;src/components&lt;/td&gt;
&lt;td&gt;组件目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;public&lt;/td&gt;
&lt;td&gt;public&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;h3&gt;1. 初始化 Astro 项目&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 创建美妙的 Astro 项目
npm create astro@latest --template uxiaohan/vhAstro-Theme astro-blog
# 进入项目目录
cd astro-blog
# 安装依赖
npm install
# 本地预览
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;迁移文章内容&lt;/h3&gt;
&lt;p&gt;:::note{type=&quot;success&quot;}
将 &lt;code&gt;Hexo&lt;/code&gt; 博客的 &lt;code&gt;src/_posts/&lt;/code&gt; 目录下的文章文件，复制到 &lt;code&gt;Astro&lt;/code&gt; 的 &lt;code&gt;src/content/blog/&lt;/code&gt; 目录下即可，然后自定义 &lt;code&gt;src/config.ts&lt;/code&gt; 配置文件去自定义博客。&lt;/p&gt;
&lt;p&gt;⚠️ &lt;code&gt;Hexo&lt;/code&gt; 的部署、使用、自动化部署等方法 完全适用于 &lt;code&gt;Astro&lt;/code&gt; 博客！&lt;/p&gt;
&lt;p&gt;🎉 此时，你已成功迁移 Hexo 博客至 Astro 博客！
:::&lt;/p&gt;
&lt;h3&gt;Vercel 自动部署&lt;/h3&gt;
&lt;p&gt;::btn[Vercel 一键部署]{link=&quot;https://vercel.com/new/clone?repository-url=https://github.com/uxiaohan/vhAstro-Theme&quot;}&lt;/p&gt;
&lt;h3&gt;Cloudflare Pages 自动部署&lt;/h3&gt;
&lt;p&gt;::btn[Cloudflare Pages 一键部署]{link=&quot;https://dash.cloudflare.com/?to=/:account/workers-and-pages/create/deploy-to-workers&amp;amp;repository=https://github.com/uxiaohan/vhAstro-Theme&quot; type=&quot;warning&quot;}&lt;/p&gt;
&lt;p&gt;其他更多部署方式（简简单单闭眼部署），请参考 &lt;a href=&quot;https://docs.astro.build/en/guides/integrations-guide/cloudflare/&quot;&gt;Astro 官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;迁移后性能对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;Hexo&lt;/th&gt;
&lt;th&gt;Astro&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;12.3s&lt;/td&gt;
&lt;td&gt;5.1s&lt;/td&gt;
&lt;td&gt;58%↑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lighthouse 性能&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;12%↑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;页面大小&lt;/td&gt;
&lt;td&gt;145KB&lt;/td&gt;
&lt;td&gt;23KB&lt;/td&gt;
&lt;td&gt;84%↓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;迁移到 Astro 后，我的技术博客实现了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 构建速度提升 2.4 倍&lt;/li&gt;
&lt;li&gt;✅ 页面性能评分全满分&lt;/li&gt;
&lt;li&gt;✅ 开发体验现代化&lt;/li&gt;
&lt;li&gt;✅ 扩展能力显著增强&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Astro主题-优雅的vhAstro-Theme【使用文档】</title><link>https://blog.wemang.com/posts/astro%E4%B8%BB%E9%A2%98%E4%BC%98%E9%9B%85%E7%9A%84vhastro-theme/</link><guid isPermaLink="true">https://blog.wemang.com/posts/astro%E4%B8%BB%E9%A2%98%E4%BC%98%E9%9B%85%E7%9A%84vhastro-theme/</guid><pubDate>Sun, 02 Mar 2025 18:18:18 GMT</pubDate><content:encoded>&lt;p&gt;:::note{type=&quot;success&quot;}
🥝 从 Z-Blog 到 Emlog，从 Typecho 到 Hexo，从动态博客到静态博客，作为一个前端，我深入了解了多种 SSG 工具，如 Hexo、Vitepress、Hugo 等，并最终锁定了 Astro 作为重构博客的选择。&lt;/p&gt;
&lt;p&gt;🍇 Astro 活跃的社区支持、广泛的现代框架兼容性、高效的性能优化、优秀的开发体验以及原生 SEO 优化，支持 Markdown/MDX 编写内容，且允许开发者混合使用 React、Vue、Svelte 等主流框架的组件，是我心目中最适合构建博客这样的以内容驱动的网站的 Web 框架。&lt;/p&gt;
&lt;p&gt;🍊 我以 Astro 为基础开发的 vhAstro-Theme 主题模版，是一款优雅的响应式博客主题，它具有简洁的设计、流畅的动画和页面过渡。
:::&lt;/p&gt;
&lt;h2&gt;🚀 vhAstro-Theme：一款基于 Astro 构建的优雅的响应式博客主题&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;「当极简主义遇上工程之美」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2025/04/1743737394560.webp&quot; alt=&quot;Astro主题 vhAstro-Theme&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;项目地址&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;1、QQ 群下载：113445803 &amp;lt;br&amp;gt;2、GitHub 下载（推荐）：https://github.com/uxiaohan/vhAstro-Theme （谢谢你的star）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;✨ 功能特性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[x] 简洁的响应式设计&lt;/li&gt;
&lt;li&gt;[x] 流畅的动画和页面过渡&lt;/li&gt;
&lt;li&gt;[x] 丝滑的阻尼滚动效果&lt;/li&gt;
&lt;li&gt;[x] 顶部Banner&lt;/li&gt;
&lt;li&gt;[x] 两列布局&lt;/li&gt;
&lt;li&gt;[x] 阅读时间&lt;/li&gt;
&lt;li&gt;[x] 字数统计&lt;/li&gt;
&lt;li&gt;[x] 代码块&lt;/li&gt;
&lt;li&gt;[x] 语法高亮&lt;/li&gt;
&lt;li&gt;[x] 图片懒加载&lt;/li&gt;
&lt;li&gt;[x] 图片灯箱&lt;/li&gt;
&lt;li&gt;[x] LivePhoto&lt;/li&gt;
&lt;li&gt;[x] LaTex 数学公式&lt;/li&gt;
&lt;li&gt;[x] 赞赏功能&lt;/li&gt;
&lt;li&gt;[x] 评论 - 内置【Twikoo、Waline】&lt;/li&gt;
&lt;li&gt;[x] 本地搜索&lt;/li&gt;
&lt;li&gt;[x] 公告&lt;/li&gt;
&lt;li&gt;[x] 标签&lt;/li&gt;
&lt;li&gt;[x] 分类&lt;/li&gt;
&lt;li&gt;[x] 归档&lt;/li&gt;
&lt;li&gt;[x] 动态&lt;/li&gt;
&lt;li&gt;[x] 圈子&lt;/li&gt;
&lt;li&gt;[x] 关于&lt;/li&gt;
&lt;li&gt;[x] 留言板&lt;/li&gt;
&lt;li&gt;[x] 友情链接&lt;/li&gt;
&lt;li&gt;[x] 推荐文章&lt;/li&gt;
&lt;li&gt;[x] 置顶文章&lt;/li&gt;
&lt;li&gt;[x] 谷歌广告&lt;/li&gt;
&lt;li&gt;[x] 侧边栏选择性展示&lt;/li&gt;
&lt;li&gt;[x] 内置 404 页面&lt;/li&gt;
&lt;li&gt;[x] Sitemap 支持&lt;/li&gt;
&lt;li&gt;[x] RSS 支持&lt;/li&gt;
&lt;li&gt;[x] 活跃的社区支持&lt;/li&gt;
&lt;li&gt;[x] 广泛的现代框架兼容性&lt;/li&gt;
&lt;li&gt;[x] 高效的性能优化&lt;/li&gt;
&lt;li&gt;[x] 优秀的开发体验&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;🚀 使用方法&lt;/h2&gt;
&lt;h3&gt;使用 Github 模板&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用此模板 &lt;a href=&quot;https://github.com/new?template_name=vhAstro-Theme&amp;amp;template_owner=uxiaohan&quot;&gt;生成新仓库或 Fork 此仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;进行本地开发，Clone 新的仓库，执行 &lt;code&gt;pnpm install&lt;/code&gt; 以安装依赖&lt;/li&gt;
&lt;li&gt;若未安装 &lt;a href=&quot;https://pnpm.io&quot;&gt;pnpm&lt;/a&gt;，执行 &lt;code&gt;npm install -g pnpm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过配置文件 &lt;code&gt;src/config.ts&lt;/code&gt; 自定义博客&lt;/li&gt;
&lt;li&gt;执行 pnpm newpost &apos;文章标题&apos; 创建新文章，并在 src/content/posts/ 目录中编辑&lt;/li&gt;
&lt;li&gt;参考官方指南将博客部署至 Vercel, Netlify,Cloudflare Pages, GitHub Pages 等&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Vercel 自动部署&lt;/h3&gt;
&lt;p&gt;::btn[Vercel 一键部署]{link=&quot;https://vercel.com/new/clone?repository-url=https://github.com/uxiaohan/vhAstro-Theme&quot;}&lt;/p&gt;
&lt;h3&gt;Cloudflare Pages 自动部署&lt;/h3&gt;
&lt;p&gt;::btn[Cloudflare Pages 一键部署]{link=&quot;https://dash.cloudflare.com/?to=/:account/workers-and-pages/create/deploy-to-workers&amp;amp;repository=https://github.com/uxiaohan/vhAstro-Theme&quot; type=&quot;warning&quot;}&lt;/p&gt;
&lt;p&gt;其他更多部署方式（简简单单闭眼部署），请参考 &lt;a href=&quot;https://docs.astro.build/en/guides/integrations-guide/cloudflare/&quot;&gt;Astro 官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;使用命令拉取模板&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 使用 pnpm
pnpm create astro@latest --template uxiaohan/vhAstro-Theme astro-blog
# 或者 yarn
yarn create astro --template uxiaohan/vhAstro-Theme astro-blog
# 或者 npm
npm create astro@latest -- --template uxiaohan/vhAstro-Theme astro-blog
# 进入项目目录
cd astro-blog
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;本地开发&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 安装依赖
pnpm install
# 本地开发
pnpm dev
# 构建静态文件
pnpm build
# 创建新文章
pnpm newpost &apos;文章标题&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;⚠️ Hexo 迁移 Astro 方法&lt;/h3&gt;
&lt;p&gt;:::note{type=&quot;success&quot;}
将 &lt;code&gt;Hexo&lt;/code&gt; 博客的 &lt;code&gt;src/_posts/&lt;/code&gt; 目录下的文章文件，复制到 &lt;code&gt;Astro&lt;/code&gt; 的 &lt;code&gt;src/content/blog/&lt;/code&gt; 目录下即可，然后自定义 &lt;code&gt;src/config.ts&lt;/code&gt; 配置文件去自定义博客。&lt;/p&gt;
&lt;p&gt;⚠️ &lt;code&gt;Hexo&lt;/code&gt; 的部署、使用、自动化部署等方法 完全适用于 &lt;code&gt;Astro&lt;/code&gt; 博客！&lt;/p&gt;
&lt;p&gt;🎉 此时，你已成功迁移 Hexo 博客至 Astro 博客！
:::&lt;/p&gt;
&lt;h2&gt;🍬 特色页面&lt;/h2&gt;
&lt;h3&gt;友情链接&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 配置文件 src/page_data/Link.ts
export default {
	// API 接口请求优先，数据格式保持和 data 一致
	api: &quot;&quot;,
	// api 为空则使用 data 静态数据
	data: [
		{
			name: &quot;韩小韩博客&quot;,
			link: &quot;https://www.vvhan.com&quot;,
			avatar: &quot;https://q1.qlogo.cn/g?b=qq&amp;amp;nk=1655466387&amp;amp;s=640&quot;,
			descr: &quot;运气是计划之内的东西.&quot;
		},
		{
			name: &quot;韩小韩API&quot;,
			link: &quot;https://api.vvhan.com&quot;,
			avatar: &quot;https://api.vvhan.com/static/images/logo.webp&quot;,
			descr: &quot;免费Web API数据接口调用服务平台.&quot;
		}
	]
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;说说动态&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 配置文件 src/page_data/Talking.ts
export default {
	// API 接口请求优先，数据格式保持和 data 一致
	api: &quot;&quot;,
	// api 为空则使用 data 静态数据
	// 注意：图片请用 vh-img-flex 类包裹
	data: [
		{
			date: &quot;2025-02-12 19:36:16&quot;,
			tags: [&quot;树&quot;, &quot;夕阳&quot;],
			content: &apos;好美🌲&amp;lt;p class=&quot;vh-img-flex&quot;&amp;gt;&amp;lt;img src=&quot;https://i0.wp.com/shp.qpic.cn/collector/1655466387/937ec070-8448-4c7b-9c8b-abd41ce892cb/0&quot;&amp;gt;&amp;lt;/p&amp;gt;&apos;
		},
		{
			date: &quot;2024-10-05 16:16:06&quot;,
			tags: [&quot;日常&quot;],
			content: &quot;记录第一条说说&quot;
		}
	]
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;圈子（需部署FreshRSS）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 配置文件 src/page_data/Friends.ts
export default {
  // API 接口请求优先，数据格式保持和 data 一致
  api: &apos;&apos;,
  // api 为空则使用 data 静态数据
  data: [
    {
      &quot;title&quot;: &quot;Astro 中使用 Lenis 增加鼠标滚动阻尼感&quot;,
      &quot;auther&quot;: &quot;韩小韩博客&quot;,
      &quot;date&quot;: &quot;2025-03-06&quot;,
      &quot;link&quot;: &quot;https://www.vvhan.com/article/Lenis-in-Astro&quot;,
      &quot;content&quot;: &quot;在移动端触控交互中，惯性滚动带来的丝滑体验已成为标配，但鼠标滚轮受限于机械结构，滚动时难免产生生硬的段落感。如何让传统滚轮操作也能获得如触控板般的阻尼反馈？Lenis库通过JavaScript模拟惯性算法，成功将”物理惯性”引入网页滚动，本文将解析其实现原理与实战应用。&quot;
    },
    {
      &quot;title&quot;: &quot;Astro 添加 Twikoo 评论组件&quot;,
      &quot;auther&quot;: &quot;韩小韩博客&quot;,
      &quot;date&quot;: &quot;2025-03-03&quot;,
      &quot;link&quot;: &quot;https://www.vvhan.com/article/astro-twikoo&quot;,
      &quot;content&quot;: &quot;Astro在使用视图过渡路由时，在跳转路由时，会导致JS文件只有在第一次进入页面时生效，所以Astro在使用视图过渡路由下Twikoo时无法正常使用的，我是单独写了一个评论组件，对Twikoo进行动态加载，然后在需要评论的页面引入的。&quot;
    },
    {
      &quot;title&quot;: &quot;Astro主题-优雅的vhAstro-Theme【使用文档】&quot;,
      &quot;auther&quot;: &quot;韩小韩博客&quot;,
      &quot;date&quot;: &quot;2025-03-02&quot;,
      &quot;link&quot;: &quot;https://www.vvhan.com/article/astro-theme-vhastro-theme&quot;,
      &quot;content&quot;: &quot;🥝从Z-Blog到Emlog，从Typecho到Hexo，从动态博客到静态博客，作为一个前端，我深入了解了多种SSG工具，如Hexo、Vitepress、Hugo等，并最终锁定了Astro作为重构博客的选择。🍇Astro活跃的社区支持、广泛的现代框架兼容性、高效的性能优化、优秀的开发体验。&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;📄 文章格式&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;---
title: 标题
categories: 分类
tags:
  - 标签1
  - 标签2
id: 文章ID
date: 文章创建日期
updated: 文章更新日期
cover: &quot;封面图URL (为空默认随机内置封面 /public/assets/images/banner)&quot;
recommend: false # 是否推荐文章
top: false # 是否置顶文章
hide: false # 是否隐藏文章
&amp;lt;!-- 页面独有 --&amp;gt;
type: &quot;links&quot; # 页面类型
comment: false # 关闭页面评论（默认开启）
---
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;✅ Lighthouse&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://uxiaohan.github.io/v2/2025/03/1742543844078.svg&quot; alt=&quot;vhAstro-Theme-Lighthouse&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;🌈 项目结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;.
├── public              =&amp;gt; 静态资源
├── script              =&amp;gt; 命令
├── src
│   ├── components      =&amp;gt; 组件
│   ├── content
│   │   └── blog        =&amp;gt; 博客文章数据
│   ├── layouts         =&amp;gt; Layout 布局
│   ├── page_data       =&amp;gt; 页面数据
│   ├── pages
│   │   ├── about                        =&amp;gt; 关于页面
│   │   ├── archives                     =&amp;gt; 归档页面
│   │   ├── article                      =&amp;gt; 文章页面
│   │   ├── categories                   =&amp;gt; 分类页面
│   │   ├── friends                      =&amp;gt; 圈子页面
│   │   ├── links                        =&amp;gt; 友链页面
│   │   ├── message                      =&amp;gt; 留言页面
│   │   ├── tag                          =&amp;gt; 标签页面
│   │   ├── talking                      =&amp;gt; 动态页面
│   │   ├── [...page].astro              =&amp;gt; 首页分页
│   │   ├── 404.astro                    =&amp;gt; 404页面
│   │   ├── robots.txt.ts                =&amp;gt; 爬虫文件
│   │   └── rss.xml.ts                   =&amp;gt; RSS文件
│   ├── plugins             =&amp;gt; 插件
│   ├── scripts             =&amp;gt; 脚本
│   ├── styles              =&amp;gt; 样式
│   ├── type                =&amp;gt; 类型
│   ├── utils               =&amp;gt; 工具
│   ├── content.config.ts   =&amp;gt; 内容配置
│   ├── config.ts           =&amp;gt; 配置
├── tsconfig.json       =&amp;gt; Typescript 配置
├── astro.config.mjs    =&amp;gt; Astro 配置
├── package.json        =&amp;gt; 依赖管理
└── pnpm-lock.yaml      =&amp;gt; 依赖锁定文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;⚙️ 项目配置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;export default {
  // 网站标题
  Title: &apos;韩小韩博客&apos;,
  // 网站地址
  Site: &apos;https://www.vvhan.com&apos;,
  // 网站副标题
  Subtitle: &apos;不曾与你分享的时间,我在进步.&apos;,
  // 网站描述
  Description: &apos;韩小韩博客 专注于前开发与相关技术的实战分享，涵盖Vue框架、Node.js、Serverless等，并涉及Node、Python、Linux、Docker等领域。同时，博客也分享作者的生活、音乐和旅行的热爱。&apos;,
  // 网站作者
  Author: &apos;.𝙃𝙖𝙣&apos;,
  // 作者头像
  Avatar: &apos;https://q1.qlogo.cn/g?b=qq&amp;amp;nk=1655466387&amp;amp;s=640&apos;,
  // 网站座右铭
  Motto: &apos;运气是计划之内东西.&apos;,
  // Cover 网站缩略图
  Cover: &apos;/assets/images/banner/072c12ec85d2d3b5.webp&apos;,
  // 网站侧边栏公告 (不填写即不开启)
  Tips: &apos;&amp;lt;p&amp;gt;欢迎光临我的博客 🎉&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;这里会分享我的日常和学习中的收集、整理及总结，希望能对你有所帮助:) 💖&amp;lt;/p&amp;gt;&apos;,
  // 首页打字机文案列表
  TypeWriteList: [
    &apos;不曾与你分享的时间,我在进步.&apos;,
    &quot;I am making progress in the time I haven&apos;t shared with you.&quot;,
  ],
  // 网站创建时间
  CreateTime: &apos;2021-09-01&apos;,
  // 顶部 Banner 配置
  HomeBanner: {
    enable: true,
    // 首页高度
    HomeHeight: &apos;38.88rem&apos;,
    // 其他页面高度
    PageHeight: &apos;28.88rem&apos;,
    // 背景
    background: &quot;url(&apos;/assets/images/home-banner.webp&apos;) no-repeat center 60%/cover&quot;,
  },
  // 博客主题配置
  Theme: {
    // 颜色请用 16 进制颜色码
    // 主题颜色
    &quot;--vh-main-color&quot;: &quot;#01C4B6&quot;,
    // 字体颜色
    &quot;--vh-font-color&quot;: &quot;#34495e&quot;,
    // 侧边栏宽度
    &quot;--vh-aside-width&quot;: &quot;318px&quot;,
    // 全局圆角
    &quot;--vh-main-radius&quot;: &quot;0.88rem&quot;,
    // 主体内容宽度
    &quot;--vh-main-max-width&quot;: &quot;1458px&quot;,
  },
  // 导航栏 (新窗口打开 newWindow: true)
  Navs: [
    // 仅支持 SVG 且 SVG 需放在 public/assets/images/svg/ 目录下，填入文件名即可 &amp;lt;不需要文件后缀名&amp;gt;（封装了 SVG 组件 为了极致压缩 SVG）
    // 建议使用 https://tabler.io/icons 直接下载 SVG
    { text: &apos;朋友&apos;, link: &apos;/links&apos;, icon: &apos;Nav_friends&apos; },
    { text: &apos;圈子&apos;, link: &apos;/friends&apos;, icon: &apos;Nav_rss&apos; },
    { text: &apos;动态&apos;, link: &apos;/talking&apos;, icon: &apos;Nav_talking&apos; },
    { text: &apos;昔日&apos;, link: &apos;/archives&apos;, icon: &apos;Nav_archives&apos; },
    { text: &apos;留言&apos;, link: &apos;/message&apos;, icon: &apos;Nav_message&apos; },
    { text: &apos;关于&apos;, link: &apos;/about&apos;, icon: &apos;Nav_about&apos; },
    { text: &apos;API&apos;, link: &apos;https://api.vvhan.com/&apos;, target: true, icon: &apos;Nav_link&apos; },
  ],
  // 侧边栏个人网站
  WebSites: [
    // 仅支持 SVG 且 SVG 需放在 public/assets/images/svg/ 目录下，填入文件名即可 &amp;lt;不需要文件后缀名&amp;gt;（封装了 SVG 组件 为了极致压缩 SVG）
    // 建议使用 https://tabler.io/icons 直接下载 SVG
    { text: &apos;Github&apos;, link: &apos;https://github.com/uxiaohan&apos;, icon: &apos;WebSite_github&apos; },
    { text: &apos;韩小韩API&apos;, link: &apos;https://api.vvhan.com&apos;, icon: &apos;WebSite_api&apos; },
    { text: &apos;每日热榜&apos;, link: &apos;https://hot.vvhan.com&apos;, icon: &apos;WebSite_hot&apos; },
    { text: &apos;骤雨重山图床&apos;, link: &apos;https://wp-cdn.4ce.cn&apos;, icon: &apos;WebSite_img&apos; },
    { text: &apos;HanAnalytics&apos;, link: &apos;https://analytics.vvhan.com&apos;, icon: &apos;WebSite_analytics&apos; },
  ],
  // 侧边栏展示
  AsideShow: {
    // 是否展示个人网站
    WebSitesShow: true,
    // 是否展示分类
    CategoriesShow: true,
    // 是否展示标签
    TagsShow: true,
    // 是否展示推荐文章
    recommendArticleShow: true
  },
  // DNS预解析地址
  DNSOptimization: [
    &apos;https://i0.wp.com&apos;,
    &apos;https://cn.cravatar.com&apos;,
    &apos;https://analytics.vvhan.com&apos;,
    &apos;https://vh-api.4ce.cn&apos;,
    &apos;https://registry.npmmirror.com&apos;,
    &apos;https://pagead2.googlesyndication.com&apos;
  ],
  // 博客音乐组件解析接口
  vhMusicApi: &apos;https://vh-api.4ce.cn/blog/meting&apos;,
  // 评论组件（只允许同时开启一个）
  Comment: {
    // Twikoo 评论
    Twikoo: {
      enable: false,
      envId: &apos;&apos;
    },
    // Waline 评论
    Waline: {
      enable: false,
      serverURL: &apos;&apos;
    }
  },
  // Han Analytics 统计（https://github.com/uxiaohan/HanAnalytics）
  HanAnalytics: { enable: true, server: &apos;https://analytics.vvhan.com&apos;, siteId: &apos;Hello-HanHexoBlog&apos; },
  // Google 广告
  GoogleAds: {
    ad_Client: &apos;&apos;, //ca-pub-xxxxxx
    // 侧边栏广告(不填不开启)
    asideAD_Slot: `&amp;lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-xxxxxx&quot; data-ad-slot=&quot;xxxxxx&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&amp;gt;&amp;lt;/ins&amp;gt;`,
    // 文章页广告(不填不开启)
    articleAD_Slot: `&amp;lt;ins class=&quot;adsbygoogle&quot; style=&quot;display:block&quot; data-ad-client=&quot;ca-pub-xxxxxx&quot; data-ad-slot=&quot;xxxxxx&quot; data-ad-format=&quot;auto&quot; data-full-width-responsive=&quot;true&quot;&amp;gt;&amp;lt;/ins&amp;gt;`
  },
  // 文章内赞赏码
  Reward: {
    // 支付宝收款码
    AliPay: &apos;/assets/images/alipay.webp&apos;,
    // 微信收款码
    WeChat: &apos;/assets/images/wechat.webp&apos;
  },
  // 页面阻尼滚动速度
  ScrollSpeed: 666
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;🌈 组件&lt;/h2&gt;
&lt;h3&gt;文本加粗&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;那个女孩子 **气喘吁吁** 的打电话和你说：我在跑步
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那个女孩子 &lt;strong&gt;气喘吁吁&lt;/strong&gt; 的打电话和你说：我在跑步&lt;/p&gt;
&lt;h3&gt;文本倾斜&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;你问她为什么有 _啪啪啪_ 的声音，她和你说：我是穿拖鞋跑步的
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你问她为什么有 &lt;em&gt;啪啪啪&lt;/em&gt; 的声音，她和你说：我是穿拖鞋跑步的&lt;/p&gt;
&lt;h3&gt;文本删除&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;你说，好的那你继续 ~~跑步~~ 吧
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你说，好的那你继续 &lt;s&gt;跑步&lt;/s&gt; 吧&lt;/p&gt;
&lt;h3&gt;行内代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;`Vscode` 是全宇宙最好的编辑器
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Vscode&lt;/code&gt; 是全宇宙最好的编辑器&lt;/p&gt;
&lt;h3&gt;引用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; 这是一个引用
&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;pre&gt;&lt;code&gt;牛肉的的营养如下：

1. 能量 (kcal)
2. 脂类 (fat)
3. 蛋白质 (protein)
4. 碳水化合物 (carbohydrate)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;牛肉的的营养如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;能量 (kcal)&lt;/li&gt;
&lt;li&gt;脂类 (fat)&lt;/li&gt;
&lt;li&gt;蛋白质 (protein)&lt;/li&gt;
&lt;li&gt;碳水化合物 (carbohydrate)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;无序列表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;- 一个女朋友
- 二个女朋友
- 三个女朋友
- ...
- N 个女朋友
&lt;/code&gt;&lt;/pre&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;N 个女朋友&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;超链接&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[百度一下，你就懂了](https://www.baidu.com)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.baidu.com&quot;&gt;百度一下，你就懂了&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;3 行 3 列的表格&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;| 表头 | 表头 | 表头 |
| :--: | :--: | :--: |
| 鸡头 | 鸭头 | 狗头 |
| 鸡头 | 鸭头 | 狗头 |
| 鸡头 | 鸭头 | 狗头 |
&lt;/code&gt;&lt;/pre&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;鸡头&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;/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;td&gt;狗头&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const obj = {
	name: &quot;hi&quot;,
	age: 18
};
// 判断某个属性是否在对象里
console.log(&quot;name&quot; in obj);
// 删除对象某个属性
console.log(delete obj.name);
// 将对象的属性名提取成数组
console.log(Object.keys(obj));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;H 标签&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- H标签，页面标题即h1，不建议文章内使用h1标签 --&amp;gt;
## H2
### H3
#### H4
##### H5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;H2&lt;/h2&gt;
&lt;h3&gt;H3&lt;/h3&gt;
&lt;h4&gt;H4&lt;/h4&gt;
&lt;h5&gt;H5&lt;/h5&gt;
&lt;h3&gt;数学公式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;% 函数式
${f(x)=a_nx^n+a_{n-1}x^{n-1}+a_{n-2}x^{n-2}}+\cdots$
% 四则运算
$2x - 5y =  8$
$3x + 9y =  -12$
$7x \times 2y \neq 3z$
% 希腊字母
$\Gamma$、$\iota$、$\sigma$、$\phi$、$\upsilon$、$\Pi$、$\Bbbk$、$\heartsuit$、$\int$、$\oint$
% 三角函数、对数、指数
$\tan$、$\sin$、$\cos$、$\lg$、$\arcsin$、$\arctan$、$\min$、$\max$、$\exp$、$\log$
% 运算符
$+$、$-$、$=$、$&amp;gt;$、$&amp;lt;$、$\times$、$\div$、$\equiv$、$\leq$、$\geq$、$\neq$
% 集合符号
$\cup$、$\cap$、$\in$、$\notin$、$\ni$、$\subset$、$\subseteq$、$\supset$、$\supseteq$、$\N$、$\Z$、$\R$、$\R$、$\infty$
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;函数式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;${f(x)=a_nx^n+a_{n-1}x^{n-1}+a_{n-2}x^{n-2}}+\cdots$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;四则运算&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$2x - 5y =  8$
$3x + 9y =  -12$
$7x \times 2y \neq 3z$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;希腊字母&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$\Gamma$、$\iota$、$\sigma$、$\phi$、$\upsilon$、$\Pi$、$\Bbbk$、$\heartsuit$、$\int$、$\oint$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;三角函数、对数、指数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$\tan$、$\sin$、$\cos$、$\lg$、$\arcsin$、$\arctan$、$\min$、$\max$、$\exp$、$\log$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;运算符&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$+$、$-$、$=$、$&amp;gt;$、$&amp;lt;$、$\times$、$\div$、$\equiv$、$\leq$、$\geq$、$\neq$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;集合符号&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$\cup$、$\cap$、$\in$、$\notin$、$\ni$、$\subset$、$\subseteq$、$\supset$、$\supseteq$、$\N$、$\Z$、$\R$、$\R$、$\infty$&lt;/p&gt;
&lt;h3&gt;按钮组件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 按钮组件 --&amp;gt;
::btn[标题]{link=&quot;URL 链接&quot;}
&amp;lt;!-- 支持类型：info、success、warning、error、import --&amp;gt;
::btn[按钮]{link=&quot;链接&quot; type=&quot;info&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;::btn[按钮]{link=&quot;https://www.baidu.com&quot;}
::btn[按钮]{link=&quot;https://www.baidu.com&quot; type=&quot;info&quot;}
::btn[按钮]{link=&quot;https://www.baidu.com&quot; type=&quot;success&quot;}
::btn[按钮]{link=&quot;https://www.baidu.com&quot; type=&quot;warning&quot;}
::btn[按钮]{link=&quot;https://www.baidu.com&quot; type=&quot;error&quot;}
::btn[按钮]{link=&quot;https://www.baidu.com&quot; type=&quot;import&quot;}&lt;/p&gt;
&lt;h3&gt;Note 组件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- note组件 --&amp;gt;
:::note
这是 note 组件 默认主题
:::
&amp;lt;!-- 支持类型：info、success、warning、error、import --&amp;gt;
:::note{type=&quot;info&quot;}
这是 note 组件 success 主题
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
这是 note 组件 默认主题
:::&lt;/p&gt;
&lt;p&gt;:::note{type=&quot;info&quot;}
这是 note 组件 info 主题
:::&lt;/p&gt;
&lt;p&gt;:::note{type=&quot;success&quot;}
这是 note 组件 success 主题
:::&lt;/p&gt;
&lt;p&gt;:::note{type=&quot;warning&quot;}
这是 note 组件 warning 主题
:::&lt;/p&gt;
&lt;p&gt;:::note{type=&quot;error&quot;}
这是 note 组件 error 主题
:::&lt;/p&gt;
&lt;p&gt;:::note{type=&quot;import&quot;}
这是 note 组件 import 主题
:::&lt;/p&gt;
&lt;h3&gt;Picture 组件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;:::picture
![Astro主题-vhAstro-Theme](https://i0.wp.com/uxiaohan.github.io/v2/2023/03/42944511.png)
![Astro主题-vhAstro-Theme](https://i0.wp.com/uxiaohan.github.io/v2/2023/03/42944511.png)
![Astro主题-vhAstro-Theme](https://i0.wp.com/uxiaohan.github.io/v2/2023/03/42944511.png)
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::picture
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2023/03/42944511.png&quot; alt=&quot;Astro主题-vhAstro-Theme&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2023/03/42944511.png&quot; alt=&quot;Astro主题-vhAstro-Theme&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2023/03/42944511.png&quot; alt=&quot;Astro主题-vhAstro-Theme&quot; /&gt;
:::&lt;/p&gt;
&lt;h3&gt;LivePhoto 组件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 纵向图片 --&amp;gt;
::vhLivePhoto{photo=&quot;https://static.vvhan.com/img/1.webp&quot; video=&quot;https://static.vvhan.com/img/1.mp4&quot; type=&quot;y&quot;}
&amp;lt;!-- 横向图片 --&amp;gt;
::vhLivePhoto{photo=&quot;https://static.vvhan.com/img/2.webp&quot; video=&quot;https://static.vvhan.com/img/2.mp4&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;::vhLivePhoto{photo=&quot;/assets/livephoto/1.webp&quot; video=&quot;/assets/livephoto/1.mp4&quot; type=&quot;y&quot;}&lt;/p&gt;
&lt;p&gt;::vhLivePhoto{photo=&quot;/assets/livephoto/2.webp&quot; video=&quot;/assets/livephoto/2.mp4&quot;}&lt;/p&gt;
&lt;h3&gt;Music 组件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- id 支持：歌曲 id / 歌单 id / 专辑 id / 搜索关键词
type 支持：song, playlist, album, search（默认值：song）
server 支持：netease, tencent, kugou, xiami, baidu（默认值：netease） --&amp;gt;
&amp;lt;!-- 单曲 --&amp;gt;
::vhMusic{id=&quot;1474697967&quot;}
&amp;lt;!-- 列表 --&amp;gt;
::vhMusic{id=&quot;173901981&quot; type=&quot;playlist&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;::vhMusic{id=&quot;1474697967&quot;}
::vhMusic{id=&quot;173901981&quot; type=&quot;playlist&quot;}&lt;/p&gt;
&lt;h3&gt;Video 组件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;::vhVideo{url=&quot;https://originfastly.jsdelivr.net/gh/uxiaohan/uxiaohan.github.io@master/v2/2022/08/index.m3u8&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;::vhVideo{url=&quot;https://originfastly.jsdelivr.net/gh/uxiaohan/uxiaohan.github.io@master/v2/2022/08/index.m3u8&quot;}&lt;/p&gt;
</content:encoded></item><item><title>【开源】Tarot-塔罗牌占卜</title><link>https://blog.wemang.com/posts/%E5%BC%80%E6%BA%90tarot-%E5%A1%94%E7%BD%97%E7%89%8C%E5%8D%A0%E5%8D%9C/</link><guid isPermaLink="true">https://blog.wemang.com/posts/%E5%BC%80%E6%BA%90tarot-%E5%A1%94%E7%BD%97%E7%89%8C%E5%8D%A0%E5%8D%9C/</guid><pubDate>Fri, 06 Dec 2024 09:37:11 GMT</pubDate><content:encoded>&lt;p&gt;:::note
塔罗牌占卜作为一种深受人们喜爱的心理探索工具，已经成为许多人自我反思与探索人生的方式之一。Tarot-Web是一款基于Web的塔罗牌占卜应用，它提供了22种不同的塔罗牌，每一张卡片都融合了传统塔罗的象征意义与AI的独特解读能力。
:::&lt;/p&gt;
&lt;h3&gt;页面截图&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/12/1733449968.webp&quot; alt=&quot;【开源】Tarot-塔罗牌占卜Tarot-Web&quot; /&gt;&lt;/p&gt;
&lt;p&gt;::btn[点击体验]{link=&quot;https://tarot.4ce.cn/&quot;}&lt;/p&gt;
&lt;h3&gt;项目地址&lt;/h3&gt;
&lt;p&gt;::btn[Tarot-Web - Github]{link=&quot;https://github.com/uxiaohan/Tarot-Web&quot;}&lt;/p&gt;
&lt;h2&gt;如何部署&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;1、准备一个 Cloudflare 账户&lt;/li&gt;
&lt;li&gt;2、Fork 本仓库，自由修改&lt;code&gt;App.vue&lt;/code&gt;和&lt;code&gt;index.html&lt;/code&gt;文件中的文案&lt;/li&gt;
&lt;li&gt;3、登录&lt;code&gt;Cloudflare Dashboard&lt;/code&gt;打开&lt;code&gt;Workers 和 Pages&lt;/code&gt;创建&lt;code&gt;Pages&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;4、&lt;code&gt;连接到Git&lt;/code&gt;选择&lt;code&gt;Github&lt;/code&gt;或&lt;code&gt;Gitlab&lt;/code&gt;中你刚刚Fork的项目，点击开始设置&lt;/li&gt;
&lt;li&gt;5、只需要修改&lt;code&gt;框架预设&lt;/code&gt;为&lt;code&gt;Vue&lt;/code&gt;即可，点击保存并部署，即可部署成功并投入使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;图片步骤&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::picture
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/07/1721640641.png&quot; alt=&quot;骤雨重山图床&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/07/1721640649.png&quot; alt=&quot;骤雨重山图床&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/07/1721640656.png&quot; alt=&quot;骤雨重山图床&quot; /&gt;
:::&lt;/p&gt;
</content:encoded></item><item><title>【开源】Web Watermark 图片添加水印在线小助手</title><link>https://blog.wemang.com/posts/%E6%8E%A8%E8%8D%90%E4%B8%80%E4%B8%AA%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AEwebwatermark%E5%9B%BE%E7%89%87%E6%B7%BB%E5%8A%A0%E6%B0%B4%E5%8D%B0%E5%9C%A8%E7%BA%BF%E5%B0%8F%E5%8A%A9%E6%89%8B/</link><guid isPermaLink="true">https://blog.wemang.com/posts/%E6%8E%A8%E8%8D%90%E4%B8%80%E4%B8%AA%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AEwebwatermark%E5%9B%BE%E7%89%87%E6%B7%BB%E5%8A%A0%E6%B0%B4%E5%8D%B0%E5%9C%A8%E7%BA%BF%E5%B0%8F%E5%8A%A9%E6%89%8B/</guid><pubDate>Thu, 21 Nov 2024 15:32:51 GMT</pubDate><content:encoded>&lt;p&gt;:::note
Web Watermark是一款在线的网页版可离线使用的安全的图片添加水印项目
:::&lt;/p&gt;
&lt;h3&gt;页面截图&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/11/1732174890.webp&quot; alt=&quot;Web Watermark&quot; /&gt;&lt;/p&gt;
&lt;p&gt;::btn[点击体验]{link=&quot;https://watermark.vvhan.com/&quot;}&lt;/p&gt;
&lt;h3&gt;项目地址&lt;/h3&gt;
&lt;p&gt;::btn[WebWatermark - Github]{link=&quot;https://github.com/uxiaohan/WebWatermark&quot;}&lt;/p&gt;
</content:encoded></item><item><title>【开源】HanAnalytics访问分析Web统计托管于（Cloudflare Pages）</title><link>https://blog.wemang.com/posts/%E5%BC%80%E6%BA%90hananalytics%E8%AE%BF%E9%97%AE%E5%88%86%E6%9E%90web%E7%BB%9F%E8%AE%A1%E6%89%98%E7%AE%A1%E4%BA%8Ecloudflare-pages/</link><guid isPermaLink="true">https://blog.wemang.com/posts/%E5%BC%80%E6%BA%90hananalytics%E8%AE%BF%E9%97%AE%E5%88%86%E6%9E%90web%E7%BB%9F%E8%AE%A1%E6%89%98%E7%AE%A1%E4%BA%8Ecloudflare-pages/</guid><pubDate>Sun, 22 Sep 2024 20:20:13 GMT</pubDate><content:encoded>&lt;p&gt;:::note
Han-Analytics 是一个简单的网络分析跟踪器和仪表板，托管在被称为赛博菩萨的 Cloudflare 上,无成本稳定运行,每天可达10万次免费统计。
域名、服务器、数据库 通通都不用! 托管在 Cloudflare Pages 上即可快速部署网站分析仪表板。
:::&lt;/p&gt;
&lt;h3&gt;页面截图&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/09/1726993735.webp&quot; alt=&quot;HanAnalytics&quot; /&gt;&lt;/p&gt;
&lt;p&gt;::btn[点击体验]{link=&quot;https://analytics.vvhan.com/&quot;}&lt;/p&gt;
&lt;h3&gt;项目地址&lt;/h3&gt;
&lt;p&gt;::btn[HanAnalytics - Github]{link=&quot;https://github.com/uxiaohan/HanAnalytics&quot; type=&quot;success&quot;}&lt;/p&gt;
&lt;h3&gt;部署&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;登录到 &lt;a href=&quot;https://dash.cloudflare.com/sign-up&quot;&gt;Cloudflare Login&lt;/a&gt;，没有的注册一个 &lt;a href=&quot;https://dash.cloudflare.com/sign-up&quot;&gt;Cloudflare SignUp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;点击 Workers 和 Pages 随便创建一个 workers 并开启 分析引擎，然后复制 workers ID 备用。&lt;/li&gt;
&lt;li&gt;创建一个 &lt;a href=&quot;https://dash.cloudflare.com/profile/api-tokens&quot;&gt;Cloudflare API token&lt;/a&gt; 备用。&lt;/li&gt;
&lt;li&gt;Fork 项目到自己的 Github 账户备用。&lt;/li&gt;
&lt;li&gt;登录 Cloudflare 并创建 Pages 项目 ，链接Github仓库，选择刚刚 Fork 的项目，架构选择Vue，填入环境变量（环境变量含义如下），部署即可。&lt;/li&gt;
&lt;li&gt;Cloudflare pages 部署完成后，在项目的&lt;code&gt;设置&lt;/code&gt;中配置&lt;code&gt;绑定&lt;/code&gt;，添加&lt;code&gt;Analytics Engine&lt;/code&gt;，变量名称填写&lt;code&gt;AnalyticsBinding&lt;/code&gt;，数据集填写&lt;code&gt;AnalyticsDataset&lt;/code&gt;并保存，重新部署！。&lt;/li&gt;
&lt;li&gt;重新部署完成后，访问 &lt;code&gt;https://xxxxxx.pages.dev&lt;/code&gt; 即可访问网站分析仪表板。（注意：首次部署生成的域名可能需要几分钟时间生效，请耐心等待）&lt;/li&gt;
&lt;li&gt;部署成功后，首次打开页面没有数据，请尽快集成到自己的网站并出现有效访问后，再次打开页面即可看到数据！&lt;/li&gt;
&lt;li&gt;新增 &lt;code&gt;密码访问&lt;/code&gt; 及 &lt;code&gt;网站白名单&lt;/code&gt;，开启密码后，输入密码可访问（默认无需密码），网站白名单功能，加白的网站才可计入统计（默认任意网站都可统计）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;环境变量说明&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Cloudflare Workers ID
CLOUDFLARE_ACCOUNT_ID = 你的 Cloudflare Workers ID

# 你的 Cloudflare API token
CLOUDFLARE_API_TOKEN = 你的 Cloudflare API token

# 网站访问密码 (不设置即无需密码访问)
CLOUDFLARE_WEBSITE_PWD = 

# 可统计的白名单 格式：  域名,WebSite|域名,WebSite，多个站点使用|分隔 例如：api.vvhan.com,Hello-Han-Api|www.vvhan.com,Hello-HanHexoBlog  (不设置即允许任何统计)
CLOUDFLARE_WEBSITE_WHITELIST = 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;绑定&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 变量名
AnalyticsBinding
# 数据集
AnalyticsDataset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::picture
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/09/1727001144.webp&quot; alt=&quot;HanAnalytics访问分析&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/09/1727001550.webp&quot; alt=&quot;HanAnalytics访问分析&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/09/1727001058.webp&quot; alt=&quot;HanAnalytics访问分析&quot; /&gt;
:::&lt;/p&gt;
&lt;p&gt;:::picture
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/09/1727001090.webp&quot; alt=&quot;HanAnalytics访问分析&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/09/1727001118.webp&quot; alt=&quot;HanAnalytics访问分析&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/09/1727001163.webp&quot; alt=&quot;HanAnalytics访问分析&quot; /&gt;
:::&lt;/p&gt;
&lt;p&gt;:::picture
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/09/1727001181.webp&quot; alt=&quot;HanAnalytics访问分析&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/12/1734595834412.webp&quot; alt=&quot;HanAnalytics访问分析&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/12/1734596343524.webp&quot; alt=&quot;HanAnalytics访问分析&quot; /&gt;
:::&lt;/p&gt;
&lt;h3&gt;集成使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 在网站底部插入以下代码即可集成网站分析仪表板
&amp;lt;script defer src=&quot;https://xxxxxx.pages.dev/tracker.min.js&quot; data-website-id=&quot;自定义网站唯一标识&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
数据问题一般是由于 Cloudflare Analytics Engine 无法访问网站导致的，请确保网站可以正常访问，并且 Cloudflare Analytics Engine 已经开启。
使用 Cloudflare Analytics Engine 数据集，它完全通过 HTTP 使用 Cloudflare 的 API 进行通信，数据完全来源于 Cloudflare Analaytics Engine 数据集中读取
Cloudflare Analytics Engine 使用抽样技术，以可承受的规模化方式实现大量数据提取/查询（这类似于大多数其他分析工具，请参阅Google Analytics 上的抽样）。您可以在此处详细了解抽样技术如何与 CF AE 配合使用。
:::&lt;/p&gt;
</content:encoded></item><item><title>基于AI的微博动态心情分析【开源】</title><link>https://blog.wemang.com/posts/%E5%9F%BA%E4%BA%8Eai%E7%9A%84%E5%BE%AE%E5%8D%9A%E5%8A%A8%E6%80%81%E5%BF%83%E6%83%85%E5%88%86%E6%9E%90%E5%BC%80%E6%BA%90/</link><guid isPermaLink="true">https://blog.wemang.com/posts/%E5%9F%BA%E4%BA%8Eai%E7%9A%84%E5%BE%AE%E5%8D%9A%E5%8A%A8%E6%80%81%E5%BF%83%E6%83%85%E5%88%86%E6%9E%90%E5%BC%80%E6%BA%90/</guid><pubDate>Wed, 04 Sep 2024 15:59:05 GMT</pubDate><content:encoded>&lt;p&gt;:::note
实时关注 Ta 的微博，并了解 Ta 微博动态的情绪心情，通过 Bark 或 PushDeer 进行通知的，基于 Nodejs 的 AI的微博动态心情分析 脚本
:::&lt;/p&gt;
&lt;h3&gt;开源地址&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/uxiaohan/WeiBo-Mood&quot;&gt;WeiBo-Mood&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cloud.siliconflow.cn/i/R83F9xkI&quot;&gt;免费AI注册&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;配置 config.js 文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  //   AI API接口地址
  chatApi: &quot;https://api.siliconflow.cn/v1/chat/completions&quot;,
  //   AI 大模型 (可根据文档更换)
  chatModel: &quot;internlm/internlm2_5-7b-chat&quot;,
  //   AI Token
  chatToken: &quot;&quot;,
  //   微博ID    https://m.weibo.cn/u/1840274303
  WB_UID: &quot;1840274303&quot;,
  //   获取最新微博频率 每间隔16分钟更新一次 (Cron 表达式)
  CRON_TIME: &quot;*/16 * * * *&quot;,
  //   Bark通知Token 不填写即不通知  https://bark.day.app/
  BARK_TOKEN: &quot;&quot;,
  //   PushDeer通知Token 不填写即不通知  https://www.pushdeer.com/
  PUSH_DEER_TOKEN: &quot;&quot;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;运行脚本&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm start
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;效果图片&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/09/1725436245.png&quot; alt=&quot;微博心情分析&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>【开源】Cloudflare优选IP➕DnsPod的DDNS自动切换</title><link>https://blog.wemang.com/posts/cloudflare%E4%BC%98%E9%80%89ipdnspod%E7%9A%84ddns%E8%87%AA%E5%8A%A8%E5%88%87%E6%8D%A2/</link><guid isPermaLink="true">https://blog.wemang.com/posts/cloudflare%E4%BC%98%E9%80%89ipdnspod%E7%9A%84ddns%E8%87%AA%E5%8A%A8%E5%88%87%E6%8D%A2/</guid><pubDate>Tue, 27 Aug 2024 15:24:10 GMT</pubDate><content:encoded>&lt;p&gt;:::note
CloudFlare是一个非常优秀的CDN服务，但是CloudFlare也有一个大的毛病——大陆访问速度很慢。国外很多网站都在使用 Cloudflare CDN，但分配给中国内地访客的IP并不友好（延迟高、丢包多、速度慢）。
虽然Cloudflare公开了所有IP段，但想要在这么多IP中找到适合自己的，怕是要累死，于是就有了这个脚本。
:::&lt;/p&gt;
&lt;h2&gt;Cloudflare优选IP 每隔15分钟更新(IPv6+IPv4)&lt;/h2&gt;
&lt;h3&gt;项目地址&lt;/h3&gt;
&lt;p&gt;::btn[CloudflareIP-dnspod-ddns]{link=&quot;https://github.com/uxiaohan/CloudflareIP-dnspod-ddns&quot;}&lt;/p&gt;
&lt;h3&gt;优化后示例站点&lt;/h3&gt;
&lt;p&gt;::btn[点击体验]{link=&quot;https://cf.vvhan.com/&quot;}&lt;/p&gt;
&lt;h2&gt;实现逻辑及局部代码&lt;/h2&gt;
&lt;h3&gt;获取优选IP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const res = await fetch(&quot;https://api.vvhan.com/tool/cf_ip&quot;);
const data = await res.json();
// 数据格式
// {
//     &quot;success&quot;: true,
//     &quot;data&quot;: {
//         &quot;v4&quot;: {
//             &quot;CM&quot;: [],
//             &quot;CU&quot;: [],
//             &quot;CT&quot;: []
//         },
//         &quot;v6&quot;: {
//             &quot;CM&quot;: [],
//             &quot;CU&quot;: [],
//             &quot;CT&quot;: []
//         }
//     }
// }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;取优选IP中的最优选 (延迟比较)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 取最优选IP IPv4
const CM_IP_V4 = data.v4.CM.reduce((minItem, currentItem) =&amp;gt; {
  return currentItem.latency &amp;lt; minItem.latency ? currentItem : minItem;
}, data.v4.CM[0]);
const CU_IP_V4 = data.v4.CU.reduce((minItem, currentItem) =&amp;gt; {
  return currentItem.latency &amp;lt; minItem.latency ? currentItem : minItem;
}, data.v4.CU[0]);
const CT_IP_V4 = data.v4.CT.reduce((minItem, currentItem) =&amp;gt; {
  return currentItem.latency &amp;lt; minItem.latency ? currentItem : minItem;
}, data.v4.CT[0]);
const DNS_DATA_V4 = { 移动: CM_IP_V4.ip, 联通: CM_IP_V4.ip, 电信: CU_IP_V4.ip, 默认: CT_IP_V4.ip };

// 取最优选IP IPv6
const CM_IP_V6 = data.v6.CM.reduce((minItem, currentItem) =&amp;gt; {
  return currentItem.latency &amp;lt; minItem.latency ? currentItem : minItem;
}, data.v6.CM[0]);
const CU_IP_V6 = data.v6.CU.reduce((minItem, currentItem) =&amp;gt; {
  return currentItem.latency &amp;lt; minItem.latency ? currentItem : minItem;
}, data.v6.CU[0]);
const CT_IP_V6 = data.v6.CT.reduce((minItem, currentItem) =&amp;gt; {
  return currentItem.latency &amp;lt; minItem.latency ? currentItem : minItem;
}, data.v6.CT[0]);
const DNS_DATA_V6 = { 移动: CM_IP_V6.ip, 联通: CM_IP_V6.ip, 电信: CU_IP_V6.ip, 默认: CT_IP_V6.ip };
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;循环替换优选IP&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;DnsPodDomainList.forEach(async i =&amp;gt; {
  try {
    const res = await client.ModifyRecord({ Domain, RecordType: i.Type, RecordLine: &quot;&quot;, RecordLineId: i.LineId, Value: i.Type == &quot;A&quot; ? DNS_DATA_V4[i.Line] : DNS_DATA_V6[i.Line], RecordId: i.RecordId, SubDomain });
    console.log(res);
  } catch (error) {
    console.log(error);
  }
});
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>【开源】骤雨重山无限存储图床托管于（Cloudflare Pages）</title><link>https://blog.wemang.com/posts/%E5%BC%80%E6%BA%90%E9%AA%A4%E9%9B%A8%E9%87%8D%E5%B1%B1%E6%97%A0%E9%99%90%E5%AD%98%E5%82%A8%E5%9B%BE%E5%BA%8A%E6%89%98%E7%AE%A1%E4%BA%8Ecloudflare-pages/</link><guid isPermaLink="true">https://blog.wemang.com/posts/%E5%BC%80%E6%BA%90%E9%AA%A4%E9%9B%A8%E9%87%8D%E5%B1%B1%E6%97%A0%E9%99%90%E5%AD%98%E5%82%A8%E5%9B%BE%E5%BA%8A%E6%89%98%E7%AE%A1%E4%BA%8Ecloudflare-pages/</guid><pubDate>Mon, 22 Jul 2024 18:11:06 GMT</pubDate><content:encoded>&lt;p&gt;:::note
在现代互联网环境中，快速稳定的图片访问是提升用户体验的重要因素之一。本文将介绍如何利用Cloudflare Pages部署稳定的无限图床Imgur，实现图片上传和访问，并进一步通过WordPress的WP.COM全球图片缓存进行加速，提高图片加载速度。可用于免费图片托管解决方案，Flickr 等替代品。
:::&lt;/p&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pages.cloudflare.com/&quot;&gt;Cloudflare Pages&lt;/a&gt; 是一个强大的静态网站托管服务，结合了 Cloudflare 的全球 CDN（内容分发网络）优势。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://imgur.com/&quot;&gt;Imgur&lt;/a&gt; 是一个免费优质的图床。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://01.wp.com/&quot;&gt;WordPress 的全球图片缓存&lt;/a&gt; 是一个高效的 CDN 服务，专门用于加速 WordPress 托管的图片内容。它利用全球分布的节点，将图片缓存并提供快速访问。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cloudflare.com/zh-cn/application-services/products/cdn/&quot;&gt;Cloudflare CDN（内容分发网络）&lt;/a&gt;是由Cloudflare提供的服务，旨在加速和保护和加速全球网络应用程序。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;页面&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/07/1721639712.png&quot; alt=&quot;骤雨重山图床&quot; /&gt;&lt;/p&gt;
&lt;p&gt;::btn[点击体验]{link=&quot;https://wp-cdn.4ce.cn/&quot;}&lt;/p&gt;
&lt;h3&gt;项目地址&lt;/h3&gt;
&lt;p&gt;::btn[ZYCS-IMG - Github]{link=&quot;https://github.com/uxiaohan/ZYCS-IMG&quot;}&lt;/p&gt;
&lt;h2&gt;如何部署&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;1、准备一个 Cloudflare 账户&lt;/li&gt;
&lt;li&gt;2、Fork 本仓库，自由修改&lt;code&gt;App.vue&lt;/code&gt;和&lt;code&gt;index.html&lt;/code&gt;文件中的文案&lt;/li&gt;
&lt;li&gt;3、登录&lt;code&gt;Cloudflare Dashboard&lt;/code&gt;打开&lt;code&gt;Workers 和 Pages&lt;/code&gt;创建&lt;code&gt;Pages&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;4、&lt;code&gt;连接到Git&lt;/code&gt;选择&lt;code&gt;Github&lt;/code&gt;或&lt;code&gt;Gitlab&lt;/code&gt;中你刚刚Fork的项目，点击开始设置&lt;/li&gt;
&lt;li&gt;5、只需要修改&lt;code&gt;框架预设&lt;/code&gt;为&lt;code&gt;Vue&lt;/code&gt;即可，点击保存并部署，即可部署成功并投入使用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;图片步骤&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::picture
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/07/1721640641.png&quot; alt=&quot;骤雨重山图床&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/07/1721640649.png&quot; alt=&quot;骤雨重山图床&quot; /&gt;
&lt;img src=&quot;https://i0.wp.com/uxiaohan.github.io/v2/2024/07/1721640656.png&quot; alt=&quot;骤雨重山图床&quot; /&gt;
:::&lt;/p&gt;
&lt;h3&gt;特点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;无限图片储存数量，你可以上传不限数量的图片到&lt;code&gt;Imgur&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;无需购买服务器，托管于&lt;code&gt;Cloudflare Pages&lt;/code&gt;上，每天10万次的请求&lt;/li&gt;
&lt;li&gt;无需购买域名，可以使用&lt;code&gt;Cloudflare Pages&lt;/code&gt; 提供的&lt;code&gt;*.pages.dev&lt;/code&gt;的免费二级域名，同时也支持绑定自定义域名&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Gorm中修改mysql主键的方法</title><link>https://blog.wemang.com/posts/go/faq/gorm%E4%B8%AD%E4%BF%AE%E6%94%B9mysql%E4%B8%BB%E9%94%AE%E7%9A%84%E6%96%B9%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/faq/gorm%E4%B8%AD%E4%BF%AE%E6%94%B9mysql%E4%B8%BB%E9%94%AE%E7%9A%84%E6%96%B9%E6%B3%95/</guid><description>gorm中修改mysql主键的方法</description><pubDate>Tue, 04 Jun 2024 01:12:25 GMT</pubDate><content:encoded>&lt;h3&gt;一、为什么要修改mysql主键策略&lt;/h3&gt;
&lt;p&gt;1、我们创建mysql数据表的时候正常操作都是采用id自增类型,但是往往会造成以下几个问题
让别人可以猜到你数据库的数据量多少,甚至可以根据当前看到的id可以手动的修改浏览器上id来访问下一条数据
如果分表后会造成主键id是一样的
2、正常的做法可以修改mysql主键策略
直接使用uuid来作为主键,但是这样不好的地方是无序的,不会根据从前往后排列
使用雪花算法生成一个唯一的id
&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;二、gorm中修改mysql表主键的方法&lt;/h3&gt;
&lt;p&gt;1、直接使用数据表的hook方法,官网地址，直接在hook里面修改主键的值
2、使用gorm插件的方式全局修改,官网地址，官网上的案例似乎有点问题，自行验证&lt;/p&gt;
&lt;h3&gt;三、使用gorm插件来自定义主键&lt;/h3&gt;
&lt;p&gt;1、选用的雪花算法第三方库,链接地址&lt;/p&gt;
&lt;p&gt;2、创建一个plugin.go的文件，我也不废话，直接贴上我自己定义的代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package common

import (
 &quot;fmt&quot;
 &quot;gin-admin-api/utils&quot;
 &quot;github.com/bwmarrin/snowflake&quot;
 &quot;gorm.io/gorm&quot;
)

type DbFieldPlugin struct{}

func (op *DbFieldPlugin) Name() string {
 return &quot;dbFieldPlugin&quot;
}

func (op *DbFieldPlugin) Initialize(db *gorm.DB) (err error) {
 // 创建字段的时候雪花算法生成id
 db.Callback().Create().Before(&quot;gorm:create&quot;).Replace(&quot;id&quot;, func(db *gorm.DB) {
  node, _ := snowflake.NewNode(1)
  id := node.Generate()
  db.Statement.SetColumn(&quot;id&quot;, fmt.Sprintf(&quot;%d&quot;, id))
  accountId := db.Statement.Context.Value(&quot;accountId&quot;)
  //fmt.Println(accountId, &quot;????&quot;)
  db.Statement.SetColumn(&quot;created_by&quot;, accountId)
 })
 // 创建人
 db.Callback().Create().Before(&quot;gorm:create&quot;).Replace(&quot;created_by&quot;, func(db *gorm.DB) {
  if db.Statement.Schema != nil {
   fmt.Println(&quot;创建的钩子函数&quot;)
   // 获取到上下文中数据
   accountId := db.Statement.Context.Value(&quot;accountId&quot;)
   fmt.Println(accountId, &quot;????&quot;)
   db.Statement.SetColumn(&quot;created_by&quot;, accountId)
  }
 })
 // 更新人
 db.Callback().Update().Before(&quot;gorm:before_update&quot;).Replace(&quot;updated_by&quot;, func(db *gorm.DB) {
  if db.Statement.Schema != nil {
   fmt.Println(&quot;修改&quot;)
   accountId := db.Statement.Context.Value(&quot;accountId&quot;)
   db.Statement.SetColumn(&quot;updated_by&quot;, accountId)
  }
 })
 db.Callback().Update().Before(&quot;gorm:after_update&quot;).Replace(&quot;updated_by1&quot;, func(db *gorm.DB) {
  fmt.Println(&quot;更新后&quot;)
  fmt.Println(db.Statement.Model, &quot;当前数据11&quot;)
  fmt.Println(utils.MapToJson(db.Statement.Dest), &quot;当前数据21&quot;)
 })
 // 删除人
 db.Callback().Delete().Before(&quot;gorm:delete&quot;).Replace(&quot;deleted_by&quot;, func(db *gorm.DB) {
  if db.Statement.Schema != nil {
   accountId := db.Statement.Context.Value(&quot;accountId&quot;)
   fmt.Println(&quot;删除1111&quot;, accountId)
   db.Statement.SetColumn(&quot;deleted_by&quot;, accountId)
  }
 })
 return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3、使用插件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;db.Use(&amp;amp;DbFieldPlugin{})
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Golang拉取Github私有库的姿势</title><link>https://blog.wemang.com/posts/go/faq/golang%E6%8B%89%E5%8F%96github%E7%A7%81%E6%9C%89%E5%BA%93%E7%9A%84%E5%A7%BF%E5%8A%BF/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/faq/golang%E6%8B%89%E5%8F%96github%E7%A7%81%E6%9C%89%E5%BA%93%E7%9A%84%E5%A7%BF%E5%8A%BF/</guid><description>Golang拉取Github私有库的姿势</description><pubDate>Mon, 13 May 2024 07:27:56 GMT</pubDate><content:encoded>&lt;p&gt;我们的 Go 项目拉取依赖时,默认使用的是 https 协议的 git clone 。因此当你的 Golang 项目位于 Github 的私有仓库时,而你本地的项目又依赖这个私有库,此时你应当先设置SSH 保证 Git 能无密码拉取到该依赖&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;
其次你还必须要设置 GOPRIVATE ,当你设置后, go get 命令在碰到该仓库时,将会不走 Go Proxy 从而进行直连。&lt;/p&gt;
&lt;p&gt;示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go env -w GOPRIVATE=github.com/xhyonline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么github.com/xhyonle 组织下的私有库在你使用 go get 命令时,都能被拉取下来,当然你一定需要设置 SSH 或者 Github Token(这两个选一个即可)&lt;/p&gt;
&lt;p&gt;GOPRIVATE ，可以填写多个值，例如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GOPRIVATE=*.corp.example.com,rsc.io/private
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GOPRIVATE=*.corp.example.com,rsc.io/private
这样 go 命令会把所有包含这个后缀的软件包，包括 git.corp.example.com/xyzzy , rsc.io/private, 和 rsc.io/private/quux 都以私有仓库来对待。&lt;/p&gt;
&lt;p&gt;使用 Github Token,使用 Token 的好处在于你可以进行 CI 集成&lt;/p&gt;
&lt;p&gt;如果你使用 Github Token 你需要自行进行 Token设置&lt;/p&gt;
&lt;p&gt;示例如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global url.&quot;https://$UserName :$Token@github.com&quot;.insteadOf &quot;https://github.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 $UserName 是你的用户名(注:是用户名,而不是登录 github 的邮箱,我的就是xhyonline)&lt;/p&gt;
&lt;p&gt;$Token 是你申请的 Token&lt;/p&gt;
&lt;p&gt;申请 Token 地址如下:&lt;a href=&quot;https://github.com/settings/tokens&quot;&gt;https://github.com/settings/tokens&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;当你设置号成功之后你可以通过下面这条命令查看是否设置成功&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global --list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然你也可以重新编辑,使用下面这条命令即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global --edit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当你使用 Github Action 进行 CI 操作时,你就可以使用 Github Token&lt;/p&gt;
&lt;p&gt;.github/workflows/github-action.yml 配置文件如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: CI构建
on:
  push:
    branches: [ main,master ]
  pull_request:
    branches: [ main ,master ] # merge到main分支时触发部署

env:
  APP_NAME: myapp # 给 APP 起一个名字

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: 检出代码
        uses: actions/checkout@master

      - name: 设置环境 Golang 环境
        uses: actions/setup-go@v2
        with:
          go-version: 1.17

      - name: 设置私有仓库和GoProxy
        run: |
          export GOPROXY=https://goproxy.io,direct
#         如果你有 github 的私有库,请自行设置 GOPRIVATE 示例如下,注下面的 UserName 是用户名,例如:xhyonline,而不是你登录 Github 的邮箱
#          git config --global url.&quot;https://${{ secret.UserName }}:${{secret.Token}}@github.com&quot;.insteadOf  &quot;https://github.com&quot;
#          export GOPRIVATE=github.com/xhyonline

      - name: CI-lint 代码质量检测
        uses: golangci/golangci-lint-action@v2
        with:
          # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
          version: v1.29

      - name: 构建 BuiLd
        run: |
          go build -o app

      - name: upx 压缩二进制文件
        uses: crazy-max/ghaction-upx@v1
        with:
          version: latest
          files: |
            app
          args: -fq

      - name: 同步文件
        uses: burnett01/rsync-deployments@5.1
        with:
          switches: -avzr --delete
          path: ./app
          remote_path: /micro-server/$APP_NAME # 发布到远程主机,当然你需要自己创建 /micro-server 目录 $APP_NAME 是全局的变量
          remote_host: ${{ secrets.Host }}
          remote_port: 22
          remote_user: root
          remote_key: ${{ secrets.DeploySecret }} # 请使用 ssh-keygen -t rsa 生成秘钥对,然后将公钥拷贝到要操纵的目标器的/root/.ssh/authorized_keys里,再把私钥黏贴到 github 后台的secret里

      - name: 执行重启命令
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.Host }}
          username: root
          key: ${{ secrets.DeploySecret }}
          port: 22
          script: | # 请自行在这里执行应用的重启命令,这里我没重启,只是查看了下构建的结果
            pwd
            ls /micro-server

      - name: 构建结果通知
        uses: zzzze/webhook-trigger@master
        if: always() # 失败成功总会发送
        with:
          data: &quot;{&apos;event_type&apos;:&apos;build-result&apos;,&apos;status&apos;:&apos;${{ job.status }}&apos;,
          &apos;repository&apos;:&apos;${{ github.repository }}&apos;,&apos;job&apos;:&apos;${{ github.job }}&apos;,
          &apos;workflow&apos;:&apos;${{ github.workflow }}&apos;}&quot;
          webhook_url: ${{ secrets.WebHookURL }}
          options: &quot;-H \&quot;Accept: application/vnd.github.everest-preview+json\&quot; -H \&quot;Authorization: token ${{ secrets.TOKEN }}\&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相关资料如下:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.phpxs.com/post/7108/?ivk_sa=1024320u&quot;&gt;http://www.phpxs.com/post/7108/?ivk_sa=1024320u&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gist.github.com/Kovrinic/ea5e7123ab5c97d451804ea222ecd78a&quot;&gt;https://gist.github.com/Kovrinic/ea5e7123ab5c97d451804ea222ecd78a&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Npm私有仓库搭建——Verdaccio</title><link>https://blog.wemang.com/posts/node/npm%E7%A7%81%E6%9C%89%E4%BB%93%E5%BA%93%E6%90%AD%E5%BB%BAverdaccio/</link><guid isPermaLink="true">https://blog.wemang.com/posts/node/npm%E7%A7%81%E6%9C%89%E4%BB%93%E5%BA%93%E6%90%AD%E5%BB%BAverdaccio/</guid><description>npm私有仓库搭建——Verdaccio</description><pubDate>Mon, 29 Apr 2024 02:49:52 GMT</pubDate><content:encoded>&lt;p&gt;众所周知，每家公司都有可能需要发布自己的私有仓库，所以要将包发布到内网而不是发布到npm的公共注册表，以下使用**&lt;code&gt;verdaccio&lt;/code&gt;**，搭建一个简单的私有npm仓库。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安装Verdaccio&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;首先，你需要在你的机器上安装**&lt;code&gt;verdaccio&lt;/code&gt;**。你可以通过npm来安装它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -g verdaccio
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;运行Verdaccio&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一旦安装完毕，你可以运行**&lt;code&gt;verdaccio&lt;/code&gt;**来启动你的私有注册表服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;verdaccio --listen 192.168.0.8:4873
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况下，它将在**&lt;code&gt;http://localhost:4873&lt;/code&gt;**上运行。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;添加用户（可选）&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你想要保护你的私有注册表，你可以添加一个用户并设置密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm adduser --registry http://localhost:4873
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;登录用户&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npm login --registry http://localhost:4873/
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;发布包&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在你可以发布你的包到你的私有注册表。确保你在项目的**&lt;code&gt;package.json&lt;/code&gt;&lt;strong&gt;文件中指定了正确的&lt;/strong&gt;&lt;code&gt;name&lt;/code&gt;&lt;strong&gt;和&lt;/strong&gt;&lt;code&gt;version&lt;/code&gt;**，然后运行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm publish --registry http://localhost:4873
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你的包现在应该已经发布到你的私有注册表，并可以通过该注册表进行安装。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安装包&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;要从你的私有注册表安装包，你可以运行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install your-package-name --registry http://localhost:4873
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新最新的包&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install [package-name]@latest --registry http://localhost:4873&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>WeRead2Notion Pro使用文档</title><link>https://blog.wemang.com/posts/tool/weread2notion-pro%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/weread2notion-pro%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3/</guid><description>WeRead2Notion-Pro使用文档</description><pubDate>Wed, 27 Mar 2024 05:35:45 GMT</pubDate><content:encoded>&lt;p&gt;WeRead2Notion-Pro 使用文档&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://malinkang.notion.site/534a7684b30e4a879269313f437f2185?pvs=4&quot;&gt;预览效果&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;{{&amp;lt; note warning &amp;gt;}}&lt;/code&gt;
Weread2Notion 和 Weread2Notion-Pro 是两个不同的项目，模板也不相同，切勿用错模板。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Weread2Notion 教程：&lt;a href=&quot;https://malinkang.com/posts/weread2notion/&quot;&gt;https://malinkang.com/posts/weread2notion/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;热力图使用教程：&lt;a href=&quot;https://malinkang.com/posts/github_heatmap/&quot;&gt;https://malinkang.com/posts/github_heatmap/&lt;/a&gt;
&lt;code&gt;{{&amp;lt; /note &amp;gt;}}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fork 工程&lt;/h3&gt;
&lt;p&gt;打开&lt;a href=&quot;https://github.com/malinkang/weread2notion-pro&quot;&gt;Weread2Notion-Pro&lt;/a&gt;，点击右上角的 Fork（顺便点个 star 谢谢）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.6t6vvuelxq.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;权限&lt;/h3&gt;
&lt;p&gt;确保你打开了读写权限。&lt;/p&gt;
&lt;p&gt;依次选择 Settings-&amp;gt;Actions-&amp;gt;General，然后下拉，找到 Workflow permissions，如果没有选中 Read and write permissions，请选中，然后点下面的 save 保存。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.361c8bjy70.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.9gwc67966p.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;获取微信读书 Cookie&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;浏览器打开&lt;a href=&quot;https://weread.qq.com/&quot;&gt;网页版微信读书&lt;/a&gt;扫码登录&lt;/li&gt;
&lt;li&gt;按 F12 进入开发者模式，依次点网络-&amp;gt;文档，然后选中 weread.qq.com，下拉找到 Cookie，复制 Cookie 值&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;{{&amp;lt; note tip &amp;gt;}}&lt;/code&gt;
如果没有内容显示，请刷新下浏览器。&lt;/p&gt;
&lt;p&gt;建议使用 Chrome 浏览器，有的小伙伴使用 QQ 浏览器拿到的 Cookie 一直不能用。
&lt;code&gt;{{&amp;lt; /note &amp;gt;}}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.6bgu79fw13.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;获取 NotionToken&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;浏览器打开&lt;a href=&quot;https://www.notion.so/my-integrations&quot;&gt;https://www.notion.so/my-integrations&lt;/a&gt;点击 New integration 按钮，输入 name 后点 Submit。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.41xtnrvnoj.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;提交完成后，进入 Secrets 页面，先点击 Show，然后点击 Copy 复制，后面需要用到&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.4n7ha2qpmn.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;复制 Notion 模板&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;浏览器打开模板&lt;a href=&quot;https://malinkang.notion.site/13d2e1548f024687a42ec68a79a01c62?pvs=4&quot;&gt;https://malinkang.notion.site/13d2e1548f024687a42ec68a79a01c62?pvs=4&lt;/a&gt;，点击右上角的 Duplicate 复制。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开你刚复制的模板，点击右上角的三个点，找到&lt;code&gt;Connections&lt;/code&gt;，然后添加你创建的 Integration。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.9dcq8hiqf6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;点击右上角的 Share，然后点击 Copy link 获取页面的链接。
&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.6wqhtkc9wr.webp&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;{{&amp;lt; note warning &amp;gt;}}&lt;/code&gt; 这里是通过数据库的名字来自动获取对应的 ID 的，所以请先不要修改数据库的名字。&lt;code&gt;{{&amp;lt; /note &amp;gt;}}&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;在 Github 的 Secrets 中添加变量&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;打开你 fork 的工程，点击 Settings-&amp;gt;Secrets and variables-&amp;gt;New repository secret&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.101xmjwfo9.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Name 输入 WEREAD_COOKIE，Secret 输入框中填入你前面获取的微信读书 Cookie，然后点击 Add secret&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.6wqhtkdecr.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;同理，继续点击 New repository secret，分别增加变量名 NOTION_TOKEN 和 NOTION_PAGE。NOTION_TOKEN 的值为前面获取的 NOTION_TOKEN 值，NOTION_PAGE 值为前面获取的页面链接，最终的结果如下图所示。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.5mnkn8vwvl.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意这三个变量名一定要填写正确，一个字母都不能错，否则会同步失败。之前遇到过有的同学 NOTION_DATABASE_ID 写成 NOTION_DATEBASE_ID。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;运行&lt;/h3&gt;
&lt;p&gt;上面配置完成之后，打开你 Fork 的项目，依次点击 Actions-&amp;gt;weread note sync-&amp;gt; Run workflow，就可以运行了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.39ky61ieyv.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;以相同的方式运行 read time sync。weread note sync 主要用来同步书籍、笔记和划线。read time sync 用来生成热力图和同步阅读时长。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.6t6vvulhkr.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;问题排查&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;可以点击你 Fork 项目的 Action，查看运行状态，绿色是成功，红色是失败。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.lvhvoq4x6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;运行成功，只代表程序没有出错，不代表就一定同步数据，比如微信 Cookie 过期就不会报错。所以如果运行成功，Notion 中没有数据的话，也可以通过下面第 2 步来查看日志&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;2.可以点进去查日志，来自行排查问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.8kzuqr5kbx.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/picx-images-hosting/raw/master/20240327/image.4ckngxfpf9.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;问题解答&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;WeRead2Notion 和 WeRead2Notion-Pro 有什么区别&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;WeRead2Notion 不支持在笔记中添加自己的笔记，每次有新笔记会删除原有的笔记。WeRead2Notion-Pro 支持添加自己的笔记，每次更新不会覆盖笔记。WeRead2Notion 功能更加简洁，同步速度更快。WeRead2Notion-Pro 支持按照年、月、周、日的阅读时长、笔记数阅读数的时间统计，支持数据可视化。所以选用哪个看个人喜好，也可以两个都用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;如何自动运行&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Github Action 提供每日定时自动运行程序的功能。之前有的朋友会问，我关了电脑会同步吗，该脚本运行在 Github 的服务器上，并不是运行在你的电脑上，所以你关机并不会影响程序自动运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;每天何时同步&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;笔记设置的是 utc 时间的 0 点同步，如果你在中国，那就是每天 8 点同步。不过据我观察，Github 这个可能有延迟。时间同步是每 3 个小时运行一次。你也可以自行修改同步时间，具体参考&lt;a href=&quot;https://docs.github.com/zh/actions/using-workflows/events-that-trigger-workflows#schedule&quot;&gt;这里&lt;/a&gt;。需要注意的是 Github 每个月 2000 分钟有免费的额度，如果改的过于频繁可能会导致额度不够。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;模板中哪些可以修改&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;Database 中的 Formula 和 Rollup 类型的属性名可以修改，其他的属性名不支持修改，因为代码中是通过属性的名字来增加修改这个属性的，修改了名字程序就无法正常运行。要修改数据库的名字需要按照以下步骤。
依次选择 Settings-&amp;gt;Secrets and variables -&amp;gt; variables-&amp;gt; New repository variable。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&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;BOOK_DATABASE_NAME&lt;/td&gt;
&lt;td&gt;书架&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REVIEW_DATABASE_NAME&lt;/td&gt;
&lt;td&gt;笔记&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BOOKMARK_DATABASE_NAME&lt;/td&gt;
&lt;td&gt;划线&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAY_DATABASE_NAME&lt;/td&gt;
&lt;td&gt;日&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WEEK_DATABASE_NAME&lt;/td&gt;
&lt;td&gt;周&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MONTH_DATABASE_NAME&lt;/td&gt;
&lt;td&gt;月&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YEAR_DATABASE_NAME&lt;/td&gt;
&lt;td&gt;年&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CATEGORY_DATABASE_NAME&lt;/td&gt;
&lt;td&gt;分类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AUTHOR_DATABASE_NAME&lt;/td&gt;
&lt;td&gt;作者&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CHAPTER_DATABASE_NAME&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;ol&gt;
&lt;li&gt;年月周天中的进度是什么&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;这里的进度我设置的是每天 1 小时，每周 7 小时，每月 30 小时，每年 365 小时。你可以自行修改公式设置你的进度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;表中的时间是什么&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;如果这本书你读完了，时间是读完的时间，如果没有读完，就是你最后阅读的时间。表中的日，周，月，年都是根据这个时间来设置的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;升级&lt;/h2&gt;
&lt;p&gt;打开你 Fork 的项目，点击 Sync fork 进行同步
&lt;img src=&quot;https://docs.github.com/assets/cb-75616/mw-1440/images/help/repository/sync-fork-dropdown.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然你也可以上面使用中提到的步骤重新来一遍。&lt;/p&gt;
</content:encoded></item><item><title>基于GithubAction自动构建Hugo博客</title><link>https://blog.wemang.com/posts/tool/%E5%9F%BA%E4%BA%8Egithubaction%E8%87%AA%E5%8A%A8%E6%9E%84%E5%BB%BAhugo%E5%8D%9A%E5%AE%A2/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/%E5%9F%BA%E4%BA%8Egithubaction%E8%87%AA%E5%8A%A8%E6%9E%84%E5%BB%BAhugo%E5%8D%9A%E5%AE%A2/</guid><description>基于GithubAction自动构建Hugo博客</description><pubDate>Wed, 27 Mar 2024 02:58:04 GMT</pubDate><content:encoded>&lt;p&gt;本文主要记录了如何配置 Github Action 实现 Hugo 博客自动部署。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/cn/actions/quickstart&quot;&gt;GitHub Actions 快速入门&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gohugo.io/getting-started/quick-start/&quot;&gt;hugo quick-start&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;1. 概述&lt;/h1&gt;
&lt;p&gt;Hugo 都是静态博客，即最终生成的是静态页面，而所谓部署就是把这些静态文件放到 web 服务器(比如 Nginx、Caddy) 的对应目录就行了。&lt;/p&gt;
&lt;p&gt;因此整个 Github Action 只需要做两件事：&lt;/p&gt;
&lt;p&gt;1）编译，生成静态文件
2）部署，把静态文件移动到合适的位置
比如放到某个云服务器上
或者放到 Github Pages
然后我们再通过 git push 来触发 Github Action 就可以了。&lt;/p&gt;
&lt;h1&gt;2. 具体实现&lt;/h1&gt;
&lt;p&gt;添加 Github Action
需要在仓库根目录下创建 &lt;code&gt;.github/workflow&lt;/code&gt;这个二级目录，然后在 workflow 下以 yml 形式配置 Github Action。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体可以参考 &lt;a href=&quot;https://github.com/lixd/lixd.github.io&quot;&gt;这个仓库&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;需要指定 action 触发条件，这里就设置为 push 触发，具体如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;on:
    push:
        branches:
            - main # Set a branch to deploy
    pull_request:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上表示在 main 分支收到 push 事件时执行该 action。&lt;/p&gt;
&lt;p&gt;如果是之前创建的仓库，可能需要改成 master 分支。&lt;/p&gt;
&lt;p&gt;另外我们可以直接在 &lt;a href=&quot;https://github.com/marketplace?type=actions&quot;&gt;marketplace&lt;/a&gt; 找别人配置好的 action 来使用，就更加方便了，以下是本教程用到的 action&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/marketplace/actions/checkout&quot;&gt;actions/checkout&lt;/a&gt;
&lt;a href=&quot;https://github.com/marketplace/actions/hugo-setup&quot;&gt;hugo-setup&lt;/a&gt;
&lt;a href=&quot;https://github.com/marketplace/actions/github-pages-action&quot;&gt;github-pages-action&lt;/a&gt;
&lt;a href=&quot;https://github.com/marketplace/actions/rsync-deployments-action&quot;&gt;rsync&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我们要做的就是把这些单独的 action 进行组合，以实现自动部署。&lt;/p&gt;
&lt;p&gt;发布到 Github Pages
静态博客可以直接用 Github Pages，比较简单，缺点就是国内访问会比较慢，甚至于直接打不开。&lt;/p&gt;
&lt;p&gt;action 文件如下&lt;/p&gt;
&lt;p&gt;name: GitHub Pages&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;on:
    push:
        branches:
            - main # Set a branch to deploy
    pull_request:

jobs:
    deploy:
        runs-on: ubuntu-20.04
        concurrency:
            group: ${{ github.workflow }}-${{ github.ref }}
        steps:
            - uses: actions/checkout@v3
              with:
                  submodules: true # Fetch Hugo themes (true OR recursive)
                  fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod

            - name: Setup Hugo
              uses: peaceiris/actions-hugo@v2
              with:
                  hugo-version: &apos;0.100.2&apos;
                  # 是否启用 hugo extend
                  extended: true

            - name: Build
              run: hugo --minify

            - name: Deploy
              uses: peaceiris/actions-gh-pages@v3
              if: ${{ github.ref == &apos;refs/heads/main&apos; }}
              with:
                  github_token: ${{ secrets.GITHUB_TOKEN }}
                  publish_dir: ./public
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整个 Action 一个包含 4 个步骤：&lt;/p&gt;
&lt;p&gt;1）拉取代码
2）准备 hugo 环境
3）使用 hugo 编译生成静态文件
4）把生成的静态文件发布到 Github Pages
其他都不需要改，唯一需要注意的是 Hugo 的版本以及是否启用 hugo 扩展。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;建议改成和自己当前使用的版本，否则可能会出现兼容性问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: &apos;0.100.2&apos;
          extended: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发布到云服务器
发布到云服务器和发布到 Github Pages 差不多，只有最后 deploy 这个步骤不一样。&lt;/p&gt;
&lt;p&gt;发布到云服务器有很多种实现方式：&lt;/p&gt;
&lt;p&gt;1）直接 scp 拷贝到对应目录
2）rsync 同步
这里用的是 rsync 方式，需要在服务器上安装 rsync。这里用的是 centos7，自带了 rsync, 只是没有启动，只需要启动就好，其他系统应该也默认安装了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable rsyncd --now
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;action 文件如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Custom Server

on:
  push:
    branches:
      - main  # Set a branch to deploy
  pull_request:

jobs:
  deploy:
    runs-on: ubuntu-20.04
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}
    steps:
      - uses: actions/checkout@v3
        with:
          submodules: true  # Fetch Hugo themes (true OR recursive)
          fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: &apos;0.100.2&apos;
          extended: true

      - name: Build
        run: hugo --minify

      - name: Deploy
        uses: burnett01/rsync-deployments@5.2
        with:
          switches: -avzr --delete
          path: ./public
          remote_path: /var/www/html/ # 需要先手动在远程主机创建该目录，否则会执行失败
          remote_host: ${{ secrets.DEPLOY_HOST }} # 远程主机 IP
          remote_port: ${{ secrets.DEPLOY_PORT }} # ssh 端口，默认为 22
          remote_user: ${{ secrets.DEPLOY_USER }} # ssh user
          remote_key: ${{ secrets.DEPLOY_KEY }} # ssh 私钥
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了安全起见，敏感数据都通过 secrets 方式引用，需要在对应仓库中创建这些 secret。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/lixd/blog/raw/master/images/blogutil/github-secret-config.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;测试
随便修改点内容，执行提交&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;echo hello &amp;gt; tmp.txt
git add .
git commit -m &quot;test action&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后打开 github action 页面查看，可以看到已经在执行了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/lixd/blog/raw/master/images/blogutil/github-actions.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点开可以查看执行日志&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/lixd/blog/raw/master/images/blogutil/github-action-detail.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;到此，整个配置就完成了，具体细节可以参考 &lt;a href=&quot;https://github.com/lixd/lixd.github.io&quot;&gt;这个仓库&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>无服务器自建短链服务Url Shorten Worker完整的部署教程</title><link>https://blog.wemang.com/posts/tool/%E6%97%A0%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%87%AA%E5%BB%BA%E7%9F%AD%E9%93%BE%E6%9C%8D%E5%8A%A1url-shorten-worker%E5%AE%8C%E6%95%B4%E7%9A%84%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/</link><guid isPermaLink="true">https://blog.wemang.com/posts/tool/%E6%97%A0%E6%9C%8D%E5%8A%A1%E5%99%A8%E8%87%AA%E5%BB%BA%E7%9F%AD%E9%93%BE%E6%9C%8D%E5%8A%A1url-shorten-worker%E5%AE%8C%E6%95%B4%E7%9A%84%E9%83%A8%E7%BD%B2%E6%95%99%E7%A8%8B/</guid><description>无服务器自建短链服务Url-Shorten-Worker完整的部署教程</description><pubDate>Tue, 19 Mar 2024 03:01:48 GMT</pubDate><content:encoded>&lt;p&gt;无服务器自建短链服务 Url-Shorten-Worker 完整的部署教程&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/crazypeace/Url-Shorten-Worker&quot;&gt;源码 GitHub&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;申请 Cloudflare 账号，略&lt;/h3&gt;
&lt;h3&gt;创建一个 KV&lt;/h3&gt;
&lt;p&gt;记得这个 KV 的名字，以 urlsrv 为例&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjebnhhOSRKgzvkla6s7hLBCSHe2slJpr1hf-DoNrXWNJIH6RCTLyoA5cJA2s4mQuTYxSfsZduWFOAH6jN-NfqbIak4wJImRpDsCOopdX6FA7kbiDZRwn10pZZZWERZ3K0y7Btqi9Chl79_Rn5g2opntlmiXkMVIDRnVuz0Eis6AXT8cCTZdMuSMOwg&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjebnhhOSRKgzvkla6s7hLBCSHe2slJpr1hf-DoNrXWNJIH6RCTLyoA5cJA2s4mQuTYxSfsZduWFOAH6jN-NfqbIak4wJImRpDsCOopdX6FA7kbiDZRwn10pZZZWERZ3K0y7Btqi9Chl79_Rn5g2opntlmiXkMVIDRnVuz0Eis6AXT8cCTZdMuSMOwg=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;查看此 KV&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhBUJuZnhvq_cBbkx1gUw0D4R___9xhJRwJbVDS0WZY5J6iaNB4xOpYpjtLME1182MWIb1-sHHMtK6wLhMz-YxtMknUE7iVGr4zyWKqLbCNLeO_sh_ePMIwH-lCL19Dvbdc1S8npBaWi5r5V51CvyLSKFoM9CTM_ddCfJeBod2K6OKfaxU-arpfgVx7&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhBUJuZnhvq_cBbkx1gUw0D4R___9xhJRwJbVDS0WZY5J6iaNB4xOpYpjtLME1182MWIb1-sHHMtK6wLhMz-YxtMknUE7iVGr4zyWKqLbCNLeO_sh_ePMIwH-lCL19Dvbdc1S8npBaWi5r5V51CvyLSKFoM9CTM_ddCfJeBod2K6OKfaxU-arpfgVx7=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;添加一个条目 Entry&lt;/h4&gt;
&lt;p&gt;密钥 key 为 password，值 value 为一个随机字符串.&lt;/p&gt;
&lt;p&gt;* password 这个 key 是在脚本中要引用的，所以要设置这个。&lt;/p&gt;
&lt;p&gt;随机字符串可以使用生成&lt;a href=&quot;http://git.io/xkcdpw&quot;&gt;网站&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;随机字符串以 yejiandianci 为例&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhfciJWtH4Bi106WPz83oSYbZ79PX9gCXkvsJUYPn97FP_WCSj7_wXhIu2Tio8rIywZQ5B1pRRpkI52uMOplpF6q5XQCWkGZK-zIvqgFYlRzDkePaf-LpgafJNZ0cUAz5kFsZ2B6msMY98lHA0flGy_TM_euMpTVBeJMFxgcnKj84AeGqDUJVU21Ulw&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhfciJWtH4Bi106WPz83oSYbZ79PX9gCXkvsJUYPn97FP_WCSj7_wXhIu2Tio8rIywZQ5B1pRRpkI52uMOplpF6q5XQCWkGZK-zIvqgFYlRzDkePaf-LpgafJNZ0cUAz5kFsZ2B6msMY98lHA0flGy_TM_euMpTVBeJMFxgcnKj84AeGqDUJVU21Ulw=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;创建 Worker 服务&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEiLc3UMWhDYvcSGYsJse4r1h-hbBhYt-V7MzOe6Tzl1it7YUsmmAYpEneGbZZ9P-2tVB7BkGY2bSUZGdX99eDACGRK-P1FSzb7NDBkvXsdxdTFkUifP5t6M3q-gB2EMJX-KlW-OWxSaKKQVMcP1MiGpMHieFef38rPa_Qe37lW401ZQub4aRnPgXZdt&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEiLc3UMWhDYvcSGYsJse4r1h-hbBhYt-V7MzOe6Tzl1it7YUsmmAYpEneGbZZ9P-2tVB7BkGY2bSUZGdX99eDACGRK-P1FSzb7NDBkvXsdxdTFkUifP5t6M3q-gB2EMJX-KlW-OWxSaKKQVMcP1MiGpMHieFef38rPa_Qe37lW401ZQub4aRnPgXZdt=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEgwkVtGRGWmzk-lDr29-GEVYWp6tvReRugVH1bKU7QKkcfaJGmDqJTW9A9eu_dKtm2Qa9-WAU67VogNd072-g9hEBe6bR8jEmmqeDm9LekHhnf_7SyBeUgNCHLzTRSsPCMvH0-EuTMbNxIeFo2xtrqbnmH2JWcBYkk3mHyGpyd036h_LCPuSC1zX4Mx&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEgwkVtGRGWmzk-lDr29-GEVYWp6tvReRugVH1bKU7QKkcfaJGmDqJTW9A9eu_dKtm2Qa9-WAU67VogNd072-g9hEBe6bR8jEmmqeDm9LekHhnf_7SyBeUgNCHLzTRSsPCMvH0-EuTMbNxIeFo2xtrqbnmH2JWcBYkk3mHyGpyd036h_LCPuSC1zX4Mx=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;设置绑定 KV&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEg_2uH-69uno099gSqDipIjI-6aKD0EH_vmAMEV58UDz87FWlThVpKLHQxV7XiOxRxMHViPjAynoifkko7jsophtQuS9p7AgAAZ-hRVeNjcuyRRQ3UlU3Rbp3-5D22ZVvu3Sqe3mff4k0qQRTwvCSL3xgzZqbHG0GwQTzD9HGOQAZDajhJpGPJt-8ZI&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEg_2uH-69uno099gSqDipIjI-6aKD0EH_vmAMEV58UDz87FWlThVpKLHQxV7XiOxRxMHViPjAynoifkko7jsophtQuS9p7AgAAZ-hRVeNjcuyRRQ3UlU3Rbp3-5D22ZVvu3Sqe3mff4k0qQRTwvCSL3xgzZqbHG0GwQTzD9HGOQAZDajhJpGPJt-8ZI=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEgxXF5orTn9cYMB_XccuNbJ64A5Q0y1DWL1wEMpuO-SlccHKLUPR79eI-ZfL1ZnXSoww7-LQcokGa1vj_-6ig8qRK2z-TEZkmsYCp-43oMKaOH6_R7MAPIIAmYwP5ZYtDPtMo-W1munfYkINBhAxA7g1sq53e9K8KTzkgbEeMsWeEdYw-zXscTsTOHB&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEgxXF5orTn9cYMB_XccuNbJ64A5Q0y1DWL1wEMpuO-SlccHKLUPR79eI-ZfL1ZnXSoww7-LQcokGa1vj_-6ig8qRK2z-TEZkmsYCp-43oMKaOH6_R7MAPIIAmYwP5ZYtDPtMo-W1munfYkINBhAxA7g1sq53e9K8KTzkgbEeMsWeEdYw-zXscTsTOHB=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;变量名称必须设置为 LINKS， KV&lt;/p&gt;
&lt;p&gt;的名字选刚刚创建的 urlsrv&lt;/p&gt;
&lt;p&gt;* LINKS 是在脚本中要引用的，所以要设置这个。换句话说，如果你使用别的脚本，可能这个变量名称就不是&lt;/p&gt;
&lt;p&gt;LINKS&lt;/p&gt;
&lt;p&gt;了。&lt;/p&gt;
&lt;h4&gt;编辑 Worker 的脚本&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEiZsgUcVCEgCMPntaeOxGVW4vUMdsR3vrjdyZPnanWkzBfPveGi2b_cIAmazyvkvdRLCWxqgyTBsEpjIW8QGXYRjTKgKWQ2u7FQok9SEfywOMaX9sYOTxAWzoxkmc0Bputj4pG_Dc6ZEmUhyrAoa-POYnL05HoQu5mYcwcK7DerVjkRLw_BDart1DIX&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEiZsgUcVCEgCMPntaeOxGVW4vUMdsR3vrjdyZPnanWkzBfPveGi2b_cIAmazyvkvdRLCWxqgyTBsEpjIW8QGXYRjTKgKWQ2u7FQok9SEfywOMaX9sYOTxAWzoxkmc0Bputj4pG_Dc6ZEmUhyrAoa-POYnL05HoQu5mYcwcK7DerVjkRLw_BDart1DIX=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;把原有的内容全部删掉&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhQDPoDkOnEBdU5XYRu3HqsgltaYbm5mFAHW156M_mipznfuktozLpKwb-3Wx-h3jkQBRGUCtiZL1Yt2NylmaGJ7unYlYJc1UvfBDhNXZbkzOz4UcuUJE9DTpPW5kMGxcC1pw9VFz1T9_xqCILShmsyfqnFYxhzi0LEJBkilQch7vGt0gs9FpaFhtDI&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEhQDPoDkOnEBdU5XYRu3HqsgltaYbm5mFAHW156M_mipznfuktozLpKwb-3Wx-h3jkQBRGUCtiZL1Yt2NylmaGJ7unYlYJc1UvfBDhNXZbkzOz4UcuUJE9DTpPW5kMGxcC1pw9VFz1T9_xqCILShmsyfqnFYxhzi0LEJBkilQch7vGt0gs9FpaFhtDI=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;换成：&lt;a href=&quot;https://github.com/crazypeace/Url-Shorten-Worker/blob/main/worker.js&quot;&gt;https://github.com/crazypeace/Url-Shorten-Worker/blob/main/worker.js&lt;/a&gt; 的内容&lt;/p&gt;
&lt;h4&gt;保存并部署&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjC_7tMH4LcpVUlggbN8k8yg1GMa1zfHIle5htJDmhCCHY8Nt0mZ_mjp1-oqQnSulpE2suMxYdDLB-ZZyiRzlo7ff_DFjUKO1EQV_aBjKtqkZ_RzTDDAyDBp8DYfB7yK9lr0FM6rjhN1dR09Qpz7T1_lbtjLBU2FzA7MpvMvNr0aFYTXqoJvBeZYaPg&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjC_7tMH4LcpVUlggbN8k8yg1GMa1zfHIle5htJDmhCCHY8Nt0mZ_mjp1-oqQnSulpE2suMxYdDLB-ZZyiRzlo7ff_DFjUKO1EQV_aBjKtqkZ_RzTDDAyDBp8DYfB7yK9lr0FM6rjhN1dR09Qpz7T1_lbtjLBU2FzA7MpvMvNr0aFYTXqoJvBeZYaPg=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;======&lt;/p&gt;
&lt;p&gt;如果要当网络记事本 PasteBin&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjAzfkAS4GS8MOIZQO-WapAv9iCKB6hdhvcCvHyvDzonQifYqN2N7CmR7yqBZc-FobmbVQaRrSrDm35-1tpASnfVLsBl-MM_RVomtTKntb2xRYMVIgtnMhgmDtS0I4zSag-C7TdVlAeZo_O8aWx_9zuXAfCoD7fMT7KVR_LjnbCJk6UaXl4KD28k42Fz2A&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjAzfkAS4GS8MOIZQO-WapAv9iCKB6hdhvcCvHyvDzonQifYqN2N7CmR7yqBZc-FobmbVQaRrSrDm35-1tpASnfVLsBl-MM_RVomtTKntb2xRYMVIgtnMhgmDtS0I4zSag-C7TdVlAeZo_O8aWx_9zuXAfCoD7fMT7KVR_LjnbCJk6UaXl4KD28k42Fz2A=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果要当图床 Image Hosting&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjslxdGW6ur_FlRnaBJOQLtBqjpFGJ9s2hPSd3BolOBuOZY2y-UG67sMLS2giOqkV1Jw4gyHYzPplcIKylGwDveDTgwwApwxNWCfv5GGsIf7OZDU7eZoS5RnQ6AMhtB1QY6oe9qke0-CyJ7qcIdM8QT1FqMjg08aunDz5D2gW0lYiTQ-U8_MjHuL-Cg1Ig&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjslxdGW6ur_FlRnaBJOQLtBqjpFGJ9s2hPSd3BolOBuOZY2y-UG67sMLS2giOqkV1Jw4gyHYzPplcIKylGwDveDTgwwApwxNWCfv5GGsIf7OZDU7eZoS5RnQ6AMhtB1QY6oe9qke0-CyJ7qcIdM8QT1FqMjg08aunDz5D2gW0lYiTQ-U8_MjHuL-Cg1Ig=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果要当网络日记本, 支持 MarkDown&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEissDi6uoPnVJWV30MxDOFu4jEdOnBbtrGjKNnh0_1z6s-gNXi96M-b0lIBTDCb5BSyzzTT_QotEDia4m20kte1wrASVGhIePKyloL1rGEhmqncBAnAtnAUsvJYC737-TxF6p0EeLLU4U2zHmFHZxxE6gR6k2Xa80rzAjTlkXEY8YgwY1s3dd0SlNqvkDQ&quot;&gt;&lt;img src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEissDi6uoPnVJWV30MxDOFu4jEdOnBbtrGjKNnh0_1z6s-gNXi96M-b0lIBTDCb5BSyzzTT_QotEDia4m20kte1wrASVGhIePKyloL1rGEhmqncBAnAtnAUsvJYC737-TxF6p0EeLLU4U2zHmFHZxxE6gR6k2Xa80rzAjTlkXEY8YgwY1s3dd0SlNqvkDQ=s16000&quot; alt=&quot;img&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;======&lt;/p&gt;
&lt;h4&gt;完&lt;/h4&gt;
&lt;p&gt;要访问 你的 worker 域名/yejiandianci 来打开使用页面&lt;/p&gt;
&lt;p&gt;如：&lt;a href=&quot;https://snowy-disk-fd82.ciys.workers.dev/yejiandianci&quot;&gt;https://snowy-disk-fd82.ciys.workers.dev/yejiandianci&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/embed/4ABLdshNOMA&quot;&gt;参考视频&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;======&lt;/p&gt;
&lt;h3&gt;后记&lt;/h3&gt;
&lt;p&gt;你可以通过&lt;a href=&quot;https://zelikk.blogspot.com/2022/05/domain-cloudflare-worker-dev.html&quot;&gt;在你自己的域名下 worker 页面添加一个路由指向 worker&lt;/a&gt;的方式来实现比如 &lt;a href=&quot;https://1way.eu.org/mtSzm6&quot;&gt;https://1way.eu.org/mtSzm6&lt;/a&gt; 替代 snowy-disk-fd82.ciys.workers.dev/yejiandianci 的效果。&lt;/p&gt;
&lt;p&gt;======&lt;/p&gt;
&lt;h3&gt;开发记录&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://zelikk.blogspot.com/2022/07/url-shorten-worker-hide-tutorial.html&quot;&gt;直接访问域名返回 404。在 KV 中设置一个 entry，保存秘密 path，只有访问这个 path 才显示使用页面&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zelikk.blogspot.com/2022/07/url-shorten-worker-custom.html&quot;&gt;支持自定义短链&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zelikk.blogspot.com/2022/07/url-shorten-worker-api-password.html&quot;&gt;API 不公开服务&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zelikk.blogspot.com/2022/08/url-shorten-worker-localstorage.html&quot;&gt;页面缓存设置过的短链&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zelikk.blogspot.com/2022/08/url-shorten-worker-bootstrap-list-group-oninput.html&quot;&gt;长链接文本框预搜索 localStorage&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zelikk.blogspot.com/2022/08/url-shorten-worker-delete-kv-localstorage.html&quot;&gt;增加删除某条短链的按钮&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zelikk.blogspot.com/2023/11/url-shorten-worker-visit-count-api-api.html&quot;&gt;访问计数功能 可查询短链 成为功能完整的短链 API 系统&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zelikk.blogspot.com/2023/11/url-shorten-worker-snapchat-mode.html&quot;&gt;阅后即焚功能, 可制作一次性二维码&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zelikk.blogspot.com/2024/01/url-shorten-worker-load-cloudflare-kv.html&quot;&gt;增加读取 KV 中全部记录的功能&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zelikk.blogspot.com/2024/01/url-shorten-worker-pastebin.html&quot;&gt;变身网络记事本 Pastebin&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zelikk.blogspot.com/2024/01/url-shorten-worker-image-hosting-base64.html&quot;&gt;变身图床 Image Hosting&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zelikk.blogspot.com/2024/02/url-shorten-worker-netjournal.html&quot;&gt;变身日记本 NetJournal 支持 Markdown 一&lt;/a&gt;
&lt;a href=&quot;https://zelikk.blogspot.com/2024/02/url-shorten-worker-netjournal-markdown.html&quot;&gt;变身日记本 NetJournal 支持 Markdown 二&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>React Hook 简介</title><link>https://blog.wemang.com/posts/web/react/react-hook-%E7%AE%80%E4%BB%8B/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/react-hook-%E7%AE%80%E4%BB%8B/</guid><description>React-Hook-简介</description><pubDate>Thu, 07 Mar 2024 03:53:42 GMT</pubDate><content:encoded>&lt;h1&gt;01 React Hook 简介&lt;/h1&gt;
&lt;p&gt;首先，欢迎你来学习React Hook，通过本教程你会了解到React Hook工作原理以及我们推荐使用Hook的理由。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;学习前提&lt;/h2&gt;
&lt;p&gt;在学习本课程之前，需要你对以下知识点有基础的了解：&lt;br /&gt;
1、React基础原理；&lt;br /&gt;
2、函数组件(Functional components)和类组件(class components)，属性传参(props)，自定义内部数据(state)，生命周期函数等；&lt;br /&gt;
3、使用谷歌浏览器且安装了“React Developer Tools”调试工具。&lt;/p&gt;
&lt;p&gt;本系列文章适合有一定React开发基础的人，若是React新手，建议先从阅读React官方中文文档学起。&lt;/p&gt;
&lt;p&gt;接下来正式开始本教程。&lt;/p&gt;
&lt;h2&gt;什么是Hooks？&lt;/h2&gt;
&lt;p&gt;Hook是React 16.8版本中新增的一个新特性，丰富扩展了原有函数组件的功能，让函数组件拥有了像类组件一样的相似特性。&lt;/p&gt;
&lt;p&gt;在之前版本中函数组件不能使用React生命周期函数，Hook本身单词意思是“钩子”，作用就是“勾住某些生命周期函数或某些数据状态，并进行某些关联触发调用”。&lt;/p&gt;
&lt;p&gt;不同的Hook(钩子)有不同的作用，可以勾住不同的“点”，比如“勾住组件更新完成对应的生命周期函数”、“勾住某props值的变化”等。&lt;/p&gt;
&lt;p&gt;正因为React有多个内置Hook，所以本小节的标题才是“什么是Hooks？”，没错，用到了Hook的复数单词Hooks。&lt;/p&gt;
&lt;h5&gt;特别提醒：在React官网中使用的是Hook，而在有些教程中使用的是Hooks。在本教程中Hook和Hooks是同一个意思，不要纠结什么时候用单数什么时候是复数&lt;/h5&gt;
&lt;h5&gt;请注意&lt;/h5&gt;
&lt;p&gt;1、尽管函数组件拥有了类组件多大多数的相似特性，但有一点除外：函数组件中没有类组件中“自定义state”的特性，因此你无法在函数组件中使用“this.state.xx”这样的代码。&lt;/p&gt;
&lt;p&gt;没有不代表功能的缺失，恰恰相反，因为当你充分了解Hooks之后，你会发现函数组件内部自定义数据状态功能远远超出类组件。&lt;/p&gt;
&lt;p&gt;2、Hooks只能运行在函数组件中，不能运行在类组件中。&lt;br /&gt;
补充：准确来说，Hooks只能运行在函数组件的“内部顶层中”，不能运行在if/for等其他函数的代码体内，不允许被if/for等包裹住。&lt;/p&gt;
&lt;p&gt;3、Hooks函数必须为纯函数，所谓纯函数就是函数内部不能修改可能影响执行结果的任意参数，确保每次执行的代码结果都是一样的。&lt;/p&gt;
&lt;h2&gt;为什么要用Hooks？&lt;/h2&gt;
&lt;p&gt;先说一下类组件的一些缺点：&lt;/p&gt;
&lt;h5&gt;缺点一：复杂且不容易理解的“this”&lt;/h5&gt;
&lt;p&gt;例如事件绑定处理函数，都需要bind(this)才可以正确执行。&lt;br /&gt;
例如想获取某些自定义属性，都需要使用this.state.xx或this.props.xx。&lt;/p&gt;
&lt;p&gt;这样造成代码不够精简，并且有些时候热更新不能正常运行。&lt;/p&gt;
&lt;h5&gt;缺点二：组件数据状态逻辑不能重用、组件之间传值过程复杂&lt;/h5&gt;
&lt;p&gt;“组件数据状态逻辑不能重用”，详细解释如下：&lt;br /&gt;
“组件数据状态”是由：定义数据、默认赋值、获取数据、修改数据、数据逻辑几个环节构成。 由于类组件中的组件数据状态state必须写在该组件构造函数内部，无法将state抽离出组件，因此别的组件如果有类似state逻辑，也必须内部自己实现一次，所以才得出“组件数据状态逻辑不能重用”的结论。&lt;/p&gt;
&lt;p&gt;“组件之间传值过程复杂”，详细解释如下：&lt;br /&gt;
React本身为单向数据流，即父组件可以传值给子组件，但子组件不允许直接修改父组件中的数据状态。&lt;/p&gt;
&lt;p&gt;子组件为了达到修改父组件中的数据状态，通常采用“高阶组件(HOC)”或“父组件暴露修改函数给子组件(render props)”这2种方式。 这2种方式都会让组件变得复杂且降低可复用性。&lt;/p&gt;
&lt;h5&gt;缺点三：复杂场景下代码难以组织在一起&lt;/h5&gt;
&lt;p&gt;复杂场景下，比如数据获取(data fetching)和事件订阅(event listeners)，相关代码难以组织在一起。&lt;/p&gt;
&lt;p&gt;“相关代码难以组织在一起”，详细解释如下：&lt;/p&gt;
&lt;p&gt;第1个“难以组织”的原因：数据获取和事件订阅被分散在不同生命周期函数中。&lt;/p&gt;
&lt;p&gt;例如数据获取：组件第一次被挂载(componentDidMount)、组件每次更新完毕(componentDidUpdate)&lt;br /&gt;
例如事件监听：组件第一次被挂载(componentDidMount)、组件即将被卸载(componentWillUnmount)&lt;/p&gt;
&lt;p&gt;第2个“难以组织”的原因：内部state数据只能是整体，无法被拆分更细致&lt;/p&gt;
&lt;p&gt;类组件中所有内部数据都被储存在this.state中，例如某个组件定义有2个内部数据 name,age，那么永远都是this.state.name、this.state.age。 name和age永远都只是this.state中的一个属性，无法做到将name和age拆分成独立对象个体。&lt;/p&gt;
&lt;p&gt;所有内部数据都储存在this.state中，当内部数据复杂时，势必增加维护this.state的难度和复杂性。&lt;/p&gt;
&lt;p&gt;“复杂场景下代码难以组织在一起”会造成另外一个延伸性问题：加大了代码自动测试难度。&lt;/p&gt;
&lt;h5&gt;Hooks是如何解决上述类组件的缺点？&lt;/h5&gt;
&lt;p&gt;如果你现在迫切想知道答案，我想对你说：恭喜你，欢迎进入Hooks的世界。&lt;/p&gt;
&lt;p&gt;类组件缺点一：复杂且不容易理解的“this”&lt;br /&gt;
Hooks解决方式：函数组件和普通JS函数非常相似，在普通JS函数中定义的变量、方法都可以不使用“this.”，而直接使用该变量或函数，因此你不再需要去关心“this”了。&lt;/p&gt;
&lt;p&gt;类组件缺点二：组件数据状态逻辑不能重用&lt;br /&gt;
Hooks解决方式：&lt;br /&gt;
通过自定义Hook，可以数据状态逻辑从组件中抽离出去，这样同一个Hook可以被多个组件使用，解决组件数据状态逻辑并不能重用的问题。&lt;/p&gt;
&lt;p&gt;类组件缺点二：组件之间传值过程复杂、缺点三：复杂场景下代码难以组织在一起&lt;br /&gt;
Hooks解决方式：&lt;br /&gt;
通过React内置的useState()函数，可以将不同数据分别从&quot;this.state&quot;中独立拆分出去。降低数据复杂度和可维护性，同时解决类组件缺点三中“内部state数据只能是整体，无法被拆分更细”的问题。&lt;/p&gt;
&lt;p&gt;通过React内置的useEffect()函数，将componentDidMount、componentDidUpdate、componentWillUncount 3个生命周期函数通过Hook(钩子)关联成1个处理函数，解决事件订阅分散在多个生命周期函数的问题。&lt;/p&gt;
&lt;h5&gt;最为关键的是，hook还能实现一些类组件根本不能实现的功能，比如全局共享数据，代替Redux&lt;/h5&gt;
&lt;p&gt;如果阅读过上面文字，你依然一头雾水，不要着急，你现在只需要对Hooks有一个大体了解即可。&lt;br /&gt;
随着后面的深入学习，你将逐个掌握Hooks的关键用法。&lt;/p&gt;
&lt;h5&gt;你只需记住一个结论：忘掉类组件，使用Hook进行函数组件开发，将是一个明智选择&lt;/h5&gt;
&lt;p&gt;&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下内容更新于 2021.01.10&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面讲解一下 React 的生命周期函数，面对如此复杂的生命周期函数，是没有必要过于了解和研究的，目前来说，一般只需学习使用 useEffect 这个 hook 即可。&lt;/p&gt;
&lt;p&gt;useEffect 这个 hook 会在稍后讲解。&lt;/p&gt;
&lt;h2&gt;React生命周期函数&lt;/h2&gt;
&lt;p&gt;React 一次状态更新，一共分为 2 个阶段、4 个生命周期。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2 个阶段：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;render阶段：包含Diff算法，计算出状态变化&lt;/li&gt;
&lt;li&gt;commit渲染阶段：ReactDom渲染器，将状态变化渲染在视图中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;4个生命周期：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Mount(第一次挂载)&lt;/li&gt;
&lt;li&gt;Update(更新)&lt;/li&gt;
&lt;li&gt;Unmount(卸载)&lt;/li&gt;
&lt;li&gt;Error(子项发生错误)&lt;/li&gt;
&lt;/ol&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;constructor&lt;/td&gt;
&lt;td&gt;Render阶段&lt;/td&gt;
&lt;td&gt;Mount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;componentWillReceiveProps&lt;/td&gt;
&lt;td&gt;Render阶段&lt;/td&gt;
&lt;td&gt;Update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getDerivedStateFromProps&lt;/td&gt;
&lt;td&gt;Render阶段&lt;/td&gt;
&lt;td&gt;并存于Moun、Update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getDerivedStateFromError&lt;/td&gt;
&lt;td&gt;Render阶段&lt;/td&gt;
&lt;td&gt;Error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shouldComponentUpdate&lt;/td&gt;
&lt;td&gt;Render阶段&lt;/td&gt;
&lt;td&gt;Update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;componentWillMount&lt;/td&gt;
&lt;td&gt;Render阶段&lt;/td&gt;
&lt;td&gt;Mount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;componentWillUpdate&lt;/td&gt;
&lt;td&gt;Render阶段&lt;/td&gt;
&lt;td&gt;Update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;render&lt;/td&gt;
&lt;td&gt;Render阶段&lt;/td&gt;
&lt;td&gt;并存于Mount、Update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;componentDidMount&lt;/td&gt;
&lt;td&gt;Commit阶段&lt;/td&gt;
&lt;td&gt;Mount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;getSnapshotBeforeUpdate&lt;/td&gt;
&lt;td&gt;Commit阶段&lt;/td&gt;
&lt;td&gt;Update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;componentDidUpdate&lt;/td&gt;
&lt;td&gt;Commit阶段&lt;/td&gt;
&lt;td&gt;Update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;componentWillUnmount&lt;/td&gt;
&lt;td&gt;Commit阶段&lt;/td&gt;
&lt;td&gt;Unmount&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;componentDidCatch&lt;/td&gt;
&lt;td&gt;Commit阶段&lt;/td&gt;
&lt;td&gt;Error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;注意事情：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;componentWillReceiveProps、componentWillMount、componentWillUpdate 这 3 个生命周期函数正在逐步被 React 官方放弃使用，不推荐继续使用这 3 个生命周期函数。&lt;/p&gt;
&lt;p&gt;与之对应的是 getDerivedStateFromProps、getDerivedStateFromError 这 2 个这是被推荐使用的。&lt;/p&gt;
&lt;p&gt;关于各个生命周期函数详细介绍，可以参考 React 官方文档：
&lt;a href=&quot;https://zh-hans.reactjs.org/docs/react-component.html#commonly-used-lifecycle-methods&quot;&gt;https://zh-hans.reactjs.org/docs/react-component.html#commonly-used-lifecycle-methods&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;补充说明：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目前并不是所有的生命周期函数都对应有 hook 函数。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;再次重复一遍，这些生命周期函数你只需大致了解，初学者只需学会 useEffect 这个 hook 即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以上内容更新于 2021.01.10&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;本节小结&lt;/h2&gt;
&lt;p&gt;1、Hook是React 16.8及以上版本才拥有的特性。&lt;br /&gt;
2、Hook只是React“增加”的概念和一些API，对原有React体系并没有任何破坏。&lt;br /&gt;
3、Hook有很多优势，比如不需要使用“this”、数据状态细致拆分、数据状态逻辑抽离出组件、代码组织更加自由灵活等。&lt;br /&gt;
4、Hook只能用于函数组件，不能用于类组件中。&lt;br /&gt;
5、Hook虽好，但React依然保留对类组件的支持，如果你就是不喜欢Hook，更偏向于继续使用类组件，那么也是可以的，只是你需要继续面对类组件的一些缺点。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，你对Hook有了一个初步概念，接下来开始学习第1个Hook函数 useState。&lt;/p&gt;
</content:encoded></item><item><title>《Go语言四十二章经》第四十三章 rpcx框架</title><link>https://blog.wemang.com/posts/go/study/42_43_rpcx/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_43_rpcx/</guid><pubDate>Mon, 26 Feb 2024 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;rpcx 框架&lt;/h1&gt;
&lt;h2&gt;rpcx 框架简介&lt;/h2&gt;
&lt;p&gt;框架rpcx包含了服务发现、负载均衡、故障转移等服务治理能力，拥有较多的特性，例如无需定义.proto文件，支持跨语言的服务调用等。目前只支持Go语言，但性能良好，可以当作微服务框架来使用。&lt;/p&gt;
&lt;p&gt;下面开始来了解下rpcx的使用，文中例子用户服务作为本篇全文的通用示例，看看利用rpcx框架来实现RPC难易程度如何。&lt;/p&gt;
&lt;p&gt;首先安装 rpcx框架：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go get -u -v github.com/smallnest/rpcx/...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于rpcx 后续服务注册中心的需要，还需要加上一些标签来安装，即使这些标签刚开始可能用不上，但建议最好都选择安装，或许最合适的安装命令是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go get -u -v -tags &quot;reuseport quic kcp zookeeper etcd consul ping&quot; github.com/smallnest/rpcx/...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;rpcx 构建服务&lt;/h2&gt;
&lt;p&gt;由于rpcx对开发的目录结构并没有强制性的规定，所以首先需要为项目规划良好的工程目录结构，下面是用户服务在rpcx中的目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;└─appservice
    └─member
        ├─cmd
        │  ├─client
        │  │      client.go
        │  │
        │  └─server
        │          server.go
        │
        ├─model
        │      member.go
        │
        └─service
                service.go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;appservice作为所有服务的总目录入口，member目录是用户服务的目录，下面cmd作为客户端和服务端入口程序的目录，model目录专门用来定义数据结构，而service作为服务的主要实现目录，存放service.go文件，在该文件中定义了用户服务的所有方法和参数类型结构。这就是单个服务整体的目录结构，当然如果有配置项还可以建立conf目录。&lt;/p&gt;
&lt;p&gt;该服务的目录结构建议在其他服务中也保持一致，这样在开发中对提升效率会有较大帮助，而且这样的约定也是在开发中非常有必要存在的。&lt;/p&gt;
&lt;p&gt;在最关键的service目录中，定义了服务的主要实现。文件service.go主要代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Args struct {
	Uid int
}

type Reply struct {
	model.User
}

type ServiceUser struct {
}

func (s *ServiceUser) UserInfo(ctx context.Context, args *Args, reply *Reply) error {
	fmt.Println(&quot;service:&quot;, args.Uid)
	reply.User.AddTime = 14990093
	reply.User.Uface = &quot;http://image.xxxx.xxx/t.gif&quot;
	reply.User.UID = int64(args.Uid)
	reply.User.UserName = &quot;Joke&quot;
	reply.User.UserType = 2
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ServiceUser作为服务结构体存在，UserInfo(ctx context.Context, args *Args, reply *Reply)方法是用户服务的方法，这个方法需要满足一定的约束：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务方法是可导出的（首字母大写）&lt;/li&gt;
&lt;li&gt;该方法必须有两个可导出或是内建类型的参数&lt;/li&gt;
&lt;li&gt;第一个参数为context.Context，第二个参数是输入参数用来接收数据，第三个参数作为输出参数且必须是指针类型&lt;/li&gt;
&lt;li&gt;方法返回类型为error&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些约束条件中除了第一个参数为context.Context，其他的条件大致与Go语言中定义的RPC方法需要满足一定的条件约束相一致。&lt;/p&gt;
&lt;p&gt;在service.go文件中还分别定义了两个可导出的结构体Args和Reply，分别作为服务方法的第二个、第三个参数的类型。这两个参数类型可自定义或是内建类型，第二个参数也就是这里的Args是输入参数（接收），第三个参数也即Reply是输出参数。&lt;/p&gt;
&lt;p&gt;对于方法UserInfo()，在实际中应该读取数据库或缓存，在这里不是讨论的重点，故直接赋值。有兴趣的读者可以进行拓展，可在model目录中来处理数据库的访问与处理。&lt;/p&gt;
&lt;p&gt;在model目录中的文件member.go定义了用户结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User struct {
	UID      int64  `json:&quot;id&quot;`
	AddTime  int64  `json:&quot;addtime&quot;`
	UserType int32  `json:&quot;utype&quot;`
	Uface    string `json:&quot;uface&quot;`
	UserName string `json:&quot;uname&quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，通过服务端程序注册该服务以及方法，server.go文件在cmd目录下server目录中，主要代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var (
	addr = flag.String(&quot;addr&quot;, &quot;localhost:8972&quot;, &quot;server address&quot;)
)

func main() {
	flag.Parse()

	s := server.NewServer()
	//s.Register(new(service.ServiceUser), &quot;&quot;)
	s.RegisterName(&quot;ServiceUser&quot;, new(service.ServiceUser), &quot;&quot;)
	err := s.Serve(&quot;tcp&quot;, *addr)
	if err != nil {
		panic(err)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先使用 NewServer() 来创建一个服务实例，再通过RegisterName()或者Register()方法注册用户服务，方便客户端从服务注册中心查找并调用用户服务，然后调用 Serve 或者 ServeHTTP 来监听客户端的请求。&lt;/p&gt;
&lt;p&gt;在rpcx框架中定义了一个非常重要和关键的结构体Server：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Server struct {
	ln                 net.Listener
	readTimeout        time.Duration
	writeTimeout       time.Duration
	gatewayHTTPServer  *http.Server
	DisableHTTPGateway bool // 禁用http调用
	DisableJSONRPC     bool // 禁用json rpc
	serviceMapMu sync.RWMutex
	serviceMap   map[string]*service
	mu         sync.RWMutex
	activeConn map[net.Conn]struct{}
	doneChan   chan struct{}
	seq        uint64
	inShutdown int32
	onShutdown []func(s *Server)
	tlsConfig *tls.Config
	options map[string]interface{}
	// CORS 选项
	corsOptions *CORSOptions 
// 所有的插件
	Plugins PluginContainer
	// AuthFunc 用来鉴权
	AuthFunc func(ctx context.Context, req *protocol.Message, token string) error
	handlerMsgNum int32
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;rpcx 启动选项&lt;/h2&gt;
&lt;p&gt;在rpcx 框架中，func NewServer(options ...OptionFn) *Server方法先实例化一个Server，然后再设置启动选项，一共提供了 3个 OptionFn 来设置启动选项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    func WithReadTimeout(readTimeout time.Duration) OptionFn
    func WithTLSConfig(cfg *tls.Config) OptionFn
    func WithWriteTimeout(writeTimeout time.Duration) OptionFn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以分别用来设置服务读超时、tls证书和写超时，也即设置结构体Server的readTimeout，tlsConfig，writeTimeout 这三个字段的值。当然这三个启动选项是可选的，可根据实际需要来决定。&lt;/p&gt;
&lt;p&gt;OptionFn 的定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type OptionFn func(*Server)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是不是感觉很眼熟！没错，这里采用的就是功能选项设计模式，利用功能选项函数很方便地修改Server实例的字段，也可以做为函数NewServer()的参数来设定启动项的值。&lt;/p&gt;
&lt;p&gt;服务注册（RegisterName()或者Register()）会通过反射机制，生成service结构体的实例，该结构体的字段中name为服务注册时的具体服务名，如没指定服务名则默认为该服务（本例中为service.ServiceUser）的类型名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type service struct {
	name     string                   // 服务名字
	rcvr     reflect.Value            // 服务方法的接收器
	typ      reflect.Type             // 接收器的类型
	method   map[string]*methodType   // 注册的方法
	function map[string]*functionType // 注册的函数
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终所有注册的服务会生成serviceMap，也即在结构体Server中定义的字段&lt;code&gt;go serviceMap   map[string]*service &lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;有关服务实例的生成和服务注册过程大致就这样。接下来完成客户端的实现，client.go文件在cmd目录下client目录中，主要代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var (
	addr = flag.String(&quot;addr&quot;, &quot;localhost:8972&quot;, &quot;server address&quot;)
)

func main() {
	flag.Parse()

	d := client.NewPeer2PeerDiscovery(&quot;tcp@&quot;+*addr, &quot;&quot;)
	xclient := client.NewXClient(&quot;ServiceUser&quot;, client.Failtry, client.RandomSelect, d, client.DefaultOption)
	defer xclient.Close()

	args := service.Args{
		Uid: 999,
	}

	reply := &amp;amp;service.Reply{}
	err := xclient.Call(context.Background(), &quot;UserInfo&quot;, args, reply)
	if err != nil {
		log.Fatalf(&quot;failed to call: %v&quot;, err)
	}

	log.Println(args.Uid, &quot;:&quot;, reply.User)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先使用 NewPeer2PeerDiscovery() 来初始化点对点的服务发现（客户端直连每个服务节点），所谓的服务发现简单点说就是找到服务器列表。上面NewPeer2PeerDiscovery() 中的参数值tcp@ipaddress:port表示通过TCP通信。&lt;/p&gt;
&lt;p&gt;在rpcx框架中可以通过TCP（tcp@ipaddress:port）、HTTP（http@ipaddress:port）、UnixDomain（unix@ipaddress:port）、QUIC（quic@ipaddress:port）和KCP（kcp@ipaddress:port）通信，而且http客户端可以通过网关或者http调用来访问rpcx服务。&lt;/p&gt;
&lt;p&gt;在rpcx中使用network @ Host: port格式表示服务地址，network 可以为 tcp ， http ，unix ，quic或kcp，而Host可以是主机名或是IP地址。&lt;/p&gt;
&lt;p&gt;接下来通过NewXClient()函数得到客户端的实例，这个客户端实例支持服务发现与服务治理，其结构体xClient如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type xClient struct {
	failMode     FailMode
	selectMode   SelectMode
	cachedClient map[string]RPCClient
	breakers     sync.Map
	servicePath  string
	option       Option
	mu        sync.RWMutex
	servers   map[string]string
	discovery ServiceDiscovery
	selector  Selector

	isShutdown bool
	auth string
	Plugins PluginContainer
	ch chan []*KVPair
	serverMessageChan chan&amp;lt;- *protocol.Message
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再看 &lt;code&gt;go func NewXClient(servicePath string, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option) XClient &lt;/code&gt; 函数的签名，servicePath 是前面服务端定义的服务名“ServiceUser”，在结构体xClient中对应字段为servicePath ，在客户端和服务端中，这两者对应的字符串需要一致才能正常调用。&lt;/p&gt;
&lt;p&gt;上面用户服务的客户端在初始化xClient时选择使用 client.Failtry 错误模式，即调用远程方法失败后再次尝试当前服务器，客户端通过随机选择client.RandomSelect的方式来选择服务器，而服务发现在这里则使用了点对点的方式，也就是直接连接到服务器，可选项为client.DefaultOption，其中默认重试次数为3，默认的编码为MsgPack。大致如图1所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/rpcx-1.png&quot; alt=&quot;rpcx-1.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图1 xClient初始化&lt;/p&gt;
&lt;p&gt;客户端在初始化时会根据参数（F.S.D.O，姑且这样称呼）来确定调用失败后处理模式、路由选择的模式、发现服务器列表以及可选配置项。&lt;/p&gt;
&lt;p&gt;FailMode和SelectMode为服务治理 (失败模式与路由选择)的选项定义。在大规模的RPC系统中，许多服务节点在提供同一个服务。客户端如何选择最合适的节点来调用呢？如果调用失败，客户端应该选择另一个节点或者立即返回错误？可以通过NewXClient()来指定具体的模式。&lt;/p&gt;
&lt;p&gt;失败处理模式FailMode仅对同步调用有效（xClient.Call），而异步调用（xClient.Go）无效，FailMode一共有下面几种值可选择：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type FailMode int

const (
	// 自动选择另一台服务器
	Failover FailMode = iota
	// 立即返回错误
	Failfast
	// 再次使用当前客户端
	Failtry
	// 如果第一台服务器在指定时间内没有快速响应，则选择另一台服务器
	Failbackup
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Failfast模式：一旦调用服务节点失败，rpcx会立即返回错误。 注意这个错误可能是网络错误或者服务异常原因造成的。&lt;/p&gt;
&lt;p&gt;Failover模式：rpcx如果遇到错误，它会尝试调用另外一个节点， 直到有服务节点能正常返回信息，或者达到最大的重试次数。 重试次数Retries在参数Option中设置， 缺省设置为3。&lt;/p&gt;
&lt;p&gt;Failtry模式：rpcx调用一个服务节点出现错误，继续重试这个节点直到节点正常返回数据或者达到最大重试次数。&lt;/p&gt;
&lt;p&gt;Failbackup模式： 如果服务节点在一定的时间内不返回结果， rpcx客户端会发送相同的请求到另外一个节点，只要在这两个节点中任一节点有返回，rpcx就算调用成功。&lt;/p&gt;
&lt;p&gt;而路由选择模式SelectMode则有下面几种情况可选择：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// SelectMode 定义从候选者中选择服务的算法
type SelectMode int
const (
	// 随机选择
	RandomSelect SelectMode = iota
	// 轮询模式
	RoundRobin
	// 加权轮询模式
	WeightedRoundRobin
	// 加权网络质量优先
	WeightedICMP
	// 一致性Hash
	ConsistentHash
	// 最近的服务器
	Closest

	// 通过用户进行选择
	SelectByUser = 1000
)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，这里的路由是针对 ServicePath 和 ServiceMethod的路由。&lt;/p&gt;
&lt;p&gt;随机模式：从服务节点中随机选择一个节点。由于节点是随机选择，所以并不能保证节点之间负载的均匀。&lt;/p&gt;
&lt;p&gt;轮询模式：从服务节点列表中逐个选择依次使用，能保证每个节点均匀被访问，在节点服务能力相差不大时适用。&lt;/p&gt;
&lt;p&gt;加权轮询模式：使用基于权重的轮询算法。&lt;/p&gt;
&lt;p&gt;网络质量优先：客户端会基于ping(ICMP) 探测各个节点的网络质量，网络质量越好则节点的权重也就越高。&lt;/p&gt;
&lt;p&gt;一致性哈希：使用 JumpConsistentHash 选择节点， 相同的servicePath, serviceMethod 和参数会路由到同一个节点上。 JumpConsistentHash 是一个快速计算一致性哈希的算法，但是有一个缺陷是它不能删除节点，如果删除节点，路由需要重新计算一致性哈希。&lt;/p&gt;
&lt;p&gt;地理位置优先：它要求服务在注册的时候要设置它所在的地理经纬度。&lt;/p&gt;
&lt;p&gt;在rpcx框架中，根据路由选择模式（SelectMode）并通过选择器（Selector）来确定具体的服务器。选择器是一个接口，其定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Selector interface {
    Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string
    UpdateServer(servers map[string]string)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述路由选择模式都已经实现选择器的接口。而且在rpcx框架中支持自定义选择器，如果上述路由选择模式不适合，可考虑实现自己的路由选择器。&lt;/p&gt;
&lt;p&gt;另外，客户端的可选配置项结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Option struct {
	Group string
	Retries int
	TLSConfig *tls.Config
	Block interface{}
	RPCPath string
	ConnectTimeout time.Duration
	ReadTimeout time.Duration
	WriteTimeout time.Duration
	BackupLatency time.Duration
	GenBreaker func() Breaker
	SerializeType protocol.SerializeType
	CompressType  protocol.CompressType
	Heartbeat         bool
	HeartbeatInterval time.Duration
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在rpcx中，已经预设了一些可选项的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var DefaultOption = Option{
	Retries:        3,
	RPCPath:        share.DefaultRPCPath,
	ConnectTimeout: 10 * time.Second,
	SerializeType:  protocol.MsgPack,
	CompressType:   protocol.None,
	BackupLatency:  10 * time.Millisecond,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Retries ：重试次数。
ConnectTimeout：连接超时
SerializeType：默认通信协议&lt;/p&gt;
&lt;p&gt;还可以设置自动的心跳来保持连接不断掉。客户端需要启用心跳选项，并且设置心跳间隔：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    option := client.DefaultOption
    option.Heartbeat = true
    option.HeartbeatInterval = time.Second
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Call()方法是客户端同步远程调用的方法，而另外的Go()方法则是异步远程调用的方法。在这里Call()方法指定调用的RPC方法为用户服务的“UserInfo”方法。当执行Call()方法时，会根据选择器确定的算法（这里是随机）来选择通过服务发现找到的服务器列表，最终确定访问的服务器，远程调用时如果失败则根据失败模式来确定下一步动作，比如上面示例的代码选择Failtry失败模式会重试三次，消息的编码采用MsgPack。当然可以通过设置Option来确定采用其他的编码方式。&lt;/p&gt;
&lt;p&gt;用户服务的客户端通过rpcx框架，使用RPC远程调用的方式来调用服务端的方法，现在分别运行服务端和客户端。&lt;/p&gt;
&lt;p&gt;在命令行运行服务端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run server.go
2019/07/26 20:50:22 server.go:174: INFO : server pid:724
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在命令行运行客户端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
2019/07/26 20:50:41 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在客户端运行后，服务端接收到客户端请求并响应，控制台会显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service: 999
2019/07/26 20:50:41 server.go:358: INFO : client has closed this connection: 127.0.0.1:60186
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当服务端停止服务后，再运行客户端程序，客户端发现调用远程方法失败，接下来会因为client.Failtry模式而重试，而可选项默认的配置是client.DefaultOption.Retries=3，表示重试的次数为三次，所以这里会重试三次而宣告失败。具体如下图2所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/rpcx-2.png&quot; alt=&quot;rpcx-2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图2 服务端无响应Failtry&lt;/p&gt;
&lt;p&gt;上图中第一次的失败是Call()方法调用失败时的信息。如果把失败模式改为Failfast，停止服务端运行，再运行客户端程序，则调用远程方法时程序会直接报错而不会去尝试重试，具体如下图3所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/rpcx-3.png&quot; alt=&quot;rpcx-3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图3 服务端无响应Failfast模式&lt;/p&gt;
&lt;p&gt;在上面用户服务中，服务注册是针对服务方法而言的，如s.RegisterName(&quot;ServiceUser&quot;, new(service.ServiceUser), &quot;&quot;)，就是将service.ServiceUser用户服务这个结构体的所有方法注册到服务中心。rpcx 也支持将纯函数注册为服务，函数必须满足的条件和前面用户服务中对方法的要求一样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;该函数是可导出的（首字母大写）&lt;/li&gt;
&lt;li&gt;该该函数必须有两个可导出或是内建类型的参数&lt;/li&gt;
&lt;li&gt;第一个参数为context.Context，第二个参数是输入参数用来接收数据，第三个参数作为输出参数且必须是指针类型&lt;/li&gt;
&lt;li&gt;函数返回类型为error&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来在用户服务的service.go文件中增加一个函数，该函数要按照上面要求定义，否则不能注册成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func UserReply(ctx context.Context, args *Args, reply *Reply) error {
	reply.User.AddTime = 10000999
	reply.User.Uface = &quot;http://image.xxxx.xxx/reply.gif&quot;
	reply.User.UID = int64(args.Uid)
	reply.User.UserName = &quot;Reply&quot;
	reply.User.UserType = 3
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在服务端，即server.go文件中增加关键的一行，注册该函数到服务中心：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s.RegisterFunction(&quot;ServiceUserFn&quot;, service.UserReply, &quot;&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面方法的第一个参数为该函数的自定义服务名，第二个参数为函数名。&lt;/p&gt;
&lt;p&gt;接下来在客户端远程调用这个函数，在clientfn.go中主要代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
	flag.Parse()

	d := client.NewPeer2PeerDiscovery(&quot;tcp@&quot;+*addr, &quot;&quot;)
	xclient := client.NewXClient(&quot;ServiceUserFn&quot;, client.Failtry, client.RandomSelect, d, client.DefaultOption)
	defer xclient.Close()

	args := service.Args{
		Uid: 888,
	}

	reply := &amp;amp;service.Reply{}
	err := xclient.Call(context.Background(), &quot;UserReply&quot;, args, reply)
	if err != nil {
		log.Fatalf(&quot;failed to call: %v&quot;, err)
	}

	log.Println(args.Uid, &quot;:&quot;, reply.User)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数的远程调用和服务方法的远程调用基本一样，接下来在命令行运行服务端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run server.go
2019/07/28 21:10:36 server.go:174: INFO : server pid:739
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在命令行运行客户端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run clientfn.go
2019/07/28 21:11:12 888 : {888 10000999 3 http://image.xxxx.xxx/reply.gif Reply}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在客户端运行后，服务端接收到客户端请求并响应，控制台会显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2019/07/28 21:11:12 server.go:358: INFO : client has closed this connection: 127.0.0.1:60209
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面用户服务的例子可以看到，rpcx框架使用非常方便，RPC调用过程整体透明，而服务发现以及治理上只需要简单做好配置即可。这些方面对开发者而言，实在是非常的贴心。&lt;/p&gt;
&lt;p&gt;当然，rpcx框架不止上面这些特征，还有其他非常值得了解的特性，下面继续来更深入了解和熟悉这款优秀的RPC框架。&lt;/p&gt;
&lt;h2&gt;服务注册中心&lt;/h2&gt;
&lt;p&gt;在rpcx框架中，服务注册中心用来实现服务发现和服务元数据的存储。在rpcx框架中支持多种服务注册中心， 并且支持进程内的注册中心，方便开发与测试。rpcx框架会自动将服务的服务名，监听地址，监听协议，权重等信息登记到注册中心，也会定时将服务的吞吐率更新到注册中心。&lt;/p&gt;
&lt;p&gt;如果服务意外中断或者宕机，服务注册中心能够监测到事件发生，服务注册中心会通知客户端该服务当前不可用，在服务调用的时候不要再选择这个服务器。&lt;/p&gt;
&lt;p&gt;客户端初始化的时候从服务注册中心得到服务器列表，然后根据不同的负载均衡模式选择合适的服务器进行服务调用，同时注册中心会通知客户端某个服务暂时不可用。&lt;/p&gt;
&lt;p&gt;服务注册中心与客户端和服务端之间的关系可见下图4：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/rpcx-4.png&quot; alt=&quot;rpcx-4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图4 服务注册中心&lt;/p&gt;
&lt;p&gt;在rpcx框架中有几种不同的服务注册中心：&lt;/p&gt;
&lt;h3&gt;一、点对点&lt;/h3&gt;
&lt;p&gt;点对点使用 NewPeer2PeerDiscovery() 来初始化服务发现，由客户端直连服务节点，客户端根据唯一服务器的地址直接连接到服务器，事实上它并没有注册中心。而由于只有一个服务节点，函数func NewXClient()在生成xClient实例时，选择器Selector的selectMode实际上并没有什么作用，因为只有一个节点什么规则最终都只会而且只能选择这个节点。&lt;/p&gt;
&lt;p&gt;上面的用户服务中，使用的就是点对点的服务注册中心，最简单直接的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;d := client.NewPeer2PeerDiscovery(&quot;tcp@&quot;+*addr, &quot;&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二、点对多&lt;/h3&gt;
&lt;p&gt;点对多顾名思义同一服务部署在多台服务器，可以使用NewMultipleServersDiscovery()来发现部署服务的多台服务器。&lt;/p&gt;
&lt;p&gt;为了测试这种服务注册，在前面用户服务的基础上建立新的服务，在appservice目录下建立membermultiple目录，暂且称为membermultiple服务。该服务业务逻辑和用户服务一样，只有服务发现有变化。&lt;/p&gt;
&lt;p&gt;假设有两台服务器来部署这个服务，为了方便测试，这里需要通过不同的端口来模拟不同的服务器，服务端文件server.go主要代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var (
	addr0 = flag.String(&quot;addr0&quot;, &quot;localhost:8972&quot;, &quot;server0 address&quot;)
	addr1 = flag.String(&quot;addr1&quot;, &quot;localhost:8973&quot;, &quot;server1 address&quot;)
)

func main() {
	flag.Parse()

	go createServer(*addr0)
	go createServer(*addr1)

	select {}
}

func createServer(addr string) {
	s := server.NewServer()

	s.RegisterName(&quot;ServiceUser&quot;, new(service.ServiceUser), &quot;&quot;)
	err := s.Serve(&quot;tcp&quot;, addr)
	if err != nil {
		panic(err)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码相当于membermultiple服务同时在两台服务器上运行，而客户端采用NewMultipleServersDiscovery()方式来得到服务器信息，这里客户端采用编码的方式来配置服务器地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var (
	addr  = flag.String(&quot;addr0&quot;, &quot;tcp@localhost:8972&quot;, &quot;server0 address&quot;)
	addr1 = flag.String(&quot;addr1&quot;, &quot;tcp@localhost:8973&quot;, &quot;server1 address&quot;)
)
        /* ......
	d := client.NewMultipleServersDiscovery([]*client.KVPair{{Key: *addr}, {Key: *addr1\}\})
	xclient := client.NewXClient(&quot;ServiceUser&quot;, client.Failtry, client.RandomSelect, d, client.DefaultOption)
	defer xclient.Close()
        ......
	*/
	err := xclient.Call(context.Background(), &quot;UserInfo&quot;, args, reply)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了更好地观察这种方式的运行，这里稍微修改下rpcx框架中的xclient.go文件，在方法selectClient()中添加	fmt.Println(&quot;===&quot;, k)语句，这里K的值是访问服务器的协议以及地址，以便观察选择器最终选择服务器的结果。&lt;/p&gt;
&lt;p&gt;接下来运行服务端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run server.go
2019/07/30 20:44:23 server.go:174: INFO : server pid:11444
2019/07/30 20:44:23 server.go:174: INFO : server pid:11444
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模拟的两个服务端已经正常运行，下面运行客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
=== tcp@localhost:8972
2019/07/30 20:46:07 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次运行客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; &amp;gt;go run client.go
=== tcp@localhost:8973
2019/07/30 20:46:41 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到两次运行客户端时，远程调用的结果一样，但访问的服务器不一样。经过多次测试发现，现在RandomSelect模式下服务器连接是随机的，并不是轮换，上面测试结果两次服务器不一样只是一种巧合。&lt;/p&gt;
&lt;p&gt;现在修改客户端代码中的NewMultipleServersDiscovery()和NewXClient()的参数值为如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	d := client.NewMultipleServersDiscovery([]*client.KVPair{{Key: *addr, Value: &quot;weight=1&quot;}, {Key: *addr1, Value: &quot;weight=9&quot;}})
	xclient := client.NewXClient(&quot;ServiceUser&quot;, client.Failtry, client.WeightedRoundRobin, d, client.DefaultOption)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改为WeightedRoundRobin按照权重轮询后，再多次运行客户端会发现，访问的服务器大多数情况都是addr1，因为它的权重是9，所以基本上都会连接到这台服务器上。&lt;/p&gt;
&lt;h3&gt;三、Etcd&lt;/h3&gt;
&lt;p&gt;说到Etcd，它是一个强一致的分布式键值存储存储系统，主要用于配置和服务发现，用它来做rpcx框架的服务注册中心是非常合适的选择。下面来具体了解怎样利用Etcd做服务注册中心。&lt;/p&gt;
&lt;p&gt;首先需要确定已经安装好Etcd，如果没有则请先安装Etcd。&lt;/p&gt;
&lt;p&gt;为了测试Etcd服务注册，在前面用户服务的基础上建立新的服务，在appservice目录下建立memberetcd目录，暂且称为memberetcd服务。该服务业务逻辑和用户服务一样，服务注册在Etcd上面。&lt;/p&gt;
&lt;p&gt;下面开始搭建memberetcd服务，服务端文件server.go主要代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var (
	addr     = flag.String(&quot;addr&quot;, &quot;localhost:8972&quot;, &quot;server address&quot;)
	etcdAddr = flag.String(&quot;etcdAddr&quot;, &quot;localhost:2379&quot;, &quot;etcd address&quot;)
	basePath = flag.String(&quot;base&quot;, &quot;/rpcx_test&quot;, &quot;prefix path&quot;)
)

func main() {
	flag.Parse()

	s := server.NewServer()
	addRegistryPlugin(s)
	//s.Register(new(service.ServiceUser), &quot;&quot;)

	s.RegisterName(&quot;ServiceUser&quot;, new(service.ServiceUser), &quot;&quot;)

	err := s.Serve(&quot;tcp&quot;, *addr)
	if err != nil {
		panic(err)
	}
}

func addRegistryPlugin(s *server.Server) {
	r := &amp;amp;serverplugin.EtcdRegisterPlugin{
		ServiceAddress: &quot;tcp@&quot; + *addr,
		EtcdServers:    []string{*etcdAddr},
		BasePath:       *basePath,
		Metrics:        metrics.NewRegistry(),
		UpdateInterval: time.Minute,
	}
	err := r.Start()
	if err != nil {
		log.Fatal(err)
	}
	s.Plugins.Add(r)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里主要实现了把服务注册到Etcd，主要通过addRegistryPlugin()函数来实现。接下来实现客户端的主要代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var (
	etcdAddr = flag.String(&quot;etcdAddr&quot;, &quot;localhost:2379&quot;, &quot;etcd address&quot;)
	basePath = flag.String(&quot;base&quot;, &quot;/rpcx_test&quot;, &quot;prefix path&quot;)
)
......
	d := client.NewEtcdDiscovery(*basePath, &quot;ServiceUser&quot;, []string{*etcdAddr}, nil)
	xclient := client.NewXClient(&quot;ServiceUser&quot;, client.Failover, client.RoundRobin, d, client.DefaultOption)
	defer xclient.Close()
	......
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里客户端采用NewEtcdDiscovery()方式发现服务。前面在安装rpcx时建议加上标签：-tags etcd，在这里也需要用到这个编译标签。在rpcx的etcd_discovery.go文件中带有编译标签：// +build etcd ，所以使用在运行或者编译时需要注意用上这个标签。&lt;/p&gt;
&lt;p&gt;首先启动Etcd服务，接下来运行服务端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run -tags etcd server.go
2019/08/05 21:22:38 server.go:174: INFO : server pid:11444
2019/08/05 21:22:38 server.go:174: INFO : server pid:11444
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模拟服务端已经正常运行，下面运行客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run -tags etcd client.go
=== tcp@localhost:8972
2019/08/05 21:25:16 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时停掉Etcd服务，再次运行客户端，则服务端和客户端都会发生错误导致程序不能正常运行。&lt;/p&gt;
&lt;p&gt;而再次启动Etcd服务，此时再运行客户端可以得到正常结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run -tags etcd client.go
=== tcp@localhost:8972
2019/08/05 21:32:06 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过Etcd客户端可以看到注册的服务信息：&lt;/p&gt;
&lt;p&gt;/rpcx_test/ServiceUser/tcp@localhost:8972&lt;/p&gt;
&lt;p&gt;Etcd作为服务注册中心是可靠的，类似像ZooKeeper、Consul都可以作为可靠的服务注册中心，由于rpcx框架已经封装好了其作为服务注册中心的使用方法，因此Etcd和它们在使用上相差无几，这里就不再列举例子说明。 需要注意的是使用run命令运行或者构建应用程序时需要带上编译标签，如上面例子中 -tags etcd。&lt;/p&gt;
&lt;h2&gt;rpcx调用&lt;/h2&gt;
&lt;p&gt;在rpcx框架中，调用有下面几种方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (c *xClient) Call(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error
func (c *xClient) Go(ctx context.Context, serviceMethod string, args interface{}, reply interface{},done chan *Call) (*Call, error)
func (c *xClient) Fork(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error
func (c *xClient) Broadcast(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go()方法是异步调用，在异步调用中，失败模式FailMode将会不起作用，它即时返回一个Call结构体实例。&lt;/p&gt;
&lt;p&gt;Call()方法是同步调用，也是最常用的调用方式，它会根据选择器确定服务器，支持失败模式FailMode，可以设置Option可选项，来进行远程调用，直到服务器返回数据或者超时。&lt;/p&gt;
&lt;p&gt;Broadcast()方法将请求发送到该服务的所有节点。如果所有的节点都正常返回才算成功。只有在所有节点没有错误的情况下， Broadcast()方法将返回其中的一个节点的返回信息。 如果有节点返回错误的话，Broadcast()方法将返回这些错误信息中的一个。失败模式FailMode和路由选择SelectMode在该方法中都不会生效，最好设置超时避免程序挂起。&lt;/p&gt;
&lt;p&gt;Fork()方法将请求发送到该服务的所有节点。如果有任何一个节点正常返回，则成功，Fork()方法将返回其中的一个节点的返回结果。 如果所有节点返回错误的话，Fork()方法将返回这些错误信息中的一个。失败模式FailMode和路由选择SelectMode在该方法中都不会生效。&lt;/p&gt;
&lt;p&gt;还是在用户服务的基础上来看看Fork()方法的实际运行情况，在用户服务目录下cmd目录中新建fork目录，作为Fork()方法的测试目录。&lt;/p&gt;
&lt;p&gt;服务端模拟两个服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var (
	addr0 = flag.String(&quot;addr0&quot;, &quot;localhost:8972&quot;, &quot;server0 address&quot;)
	addr1 = flag.String(&quot;addr1&quot;, &quot;localhost:8973&quot;, &quot;server1 address&quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端使用多点服务发现，再使用Fork()方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var (
	addr  = flag.String(&quot;addr0&quot;, &quot;tcp@localhost:8972&quot;, &quot;server0 address&quot;)
	addr1 = flag.String(&quot;addr1&quot;, &quot;tcp@localhost:8973&quot;, &quot;server1 address&quot;)
)

func main() {
	flag.Parse()

	d := client.NewMultipleServersDiscovery([]*client.KVPair{{Key: *addr, Value: &quot;weight=1&quot;}, {Key: *addr1, Value: &quot;weight=9&quot;}})
	xclient := client.NewXClient(&quot;ServiceUser&quot;, client.Failtry, client.WeightedRoundRobin, d, client.DefaultOption)

	defer xclient.Close()

	args := service.Args{
		Uid: 999,
	}

	reply := &amp;amp;service.Reply{}
	err := xclient.Fork(context.Background(), &quot;UserInfo&quot;, args, reply)
	if err != nil {
		log.Fatalf(&quot;failed to call: %v&quot;, err)
	}

	log.Println(args.Uid, &quot;:&quot;, reply.User)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先在命令行运行服务端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run server.go
2019/08/17 15:47:36 server.go:174: INFO : server pid:9956
2019/08/17 15:47:36 server.go:174: INFO : server pid:9956
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后运行客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
2019/08/17 15:47:50 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而此时服务端控制台显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service: 999
service: 999
2019/08/17 15:47:50 server.go:358: INFO : client has closed this connection: 127.0.0.1:58487
2019/08/17 15:47:50 server.go:358: INFO : client has closed this connection: 127.0.0.1:58489
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表明Fork()方法将请求发送到了这两个服务器，得到正常结果后返回。而当服务器全部停止服务时，则Fork()方法直接报ErrXClientNoServer错误信息。&lt;/p&gt;
&lt;p&gt;对于RPC来说，序列化对于远程调用的响应速度、吞吐量、网络带宽消耗等也起着至关重要的作用，是分布式系统性能提升的关键因素之一。在rpcx框架中，默认使用 msgpack 编解码器，一共有下面几种编解码器：&lt;/p&gt;
&lt;p&gt;SerializeNone：不会对数据进行编解码，要求数据为 []byte 类型。
JSON：通用的数据交换的格式，常规情况下可使用这种编解码。
protocol buffers：一种高性能的编解码器。
MsgPack：默认的编解码器，一种高性能的编解码器，是跨语言的编解码器。
Thrift：一种高性能的编解码器。&lt;/p&gt;
&lt;p&gt;开发中可以设置Option.SerializeType来指定合适的编解码器。对于有特殊要求的场景，还可以定制新的编解码器。&lt;/p&gt;
&lt;p&gt;编解码也即序列化/反序列化，在rpcx中需要将消息结构体序列化为二进制数据，同时也需要将网络流数据反序列化为内部使用的消息结构体。大致如下图5所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/rpcx-5.png&quot; alt=&quot;rpcx-5.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图5 rpcx编解码&lt;/p&gt;
&lt;p&gt;由于在gRPC中只能使用ProtoBuf，因此看看在rpcx中怎样来使用ProtoBuf，下面基于前面的用户服务来实现，新建服务memberproto。&lt;/p&gt;
&lt;p&gt;由于有ProtoBuf，建立pb目录来存放member.proto文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

package pb;

message Args {
  int64 Id = 1;
}

message Reply {
   int64	UID =1;	
  int64	AddTime=2;
  int32	UserType=3;
  string Uface =4;
  string UserName=5;
}

message ProtoArgs { 
    int32 A = 1;
    int32 B = 2;
}

message ProtoReply { 
    int32 C = 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protoc --go_out=. member.proto
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到member.pb.go文件，接下来修改service.g文件代码，这次增加了一个方法Mul()：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (s *ServiceUser) UserInfo(ctx context.Context, args *pb.Args, reply *pb.Reply) error {
	fmt.Println(&quot;service:&quot;, args.Id)
	reply.AddTime = 14990093
	reply.Uface = &quot;http://image.xxxx.xxx/t.gif&quot;
	reply.UID = int64(args.Id)
	reply.UserName = &quot;Joke&quot;
	reply.UserType = 2
	return nil
}

func (t *ServiceUser) Mul(ctx context.Context, args *pb.ProtoArgs, reply *pb.ProtoReply) error {
	reply.C = args.A * args.B
	fmt.Printf(&quot;call: %d * %d = %d\n&quot;, args.A, args.B, reply.C)
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端需要修改Option可选项，把默认的编解码器改为protocol.ProtoBuffer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
	flag.Parse()

	// register customized codec
	option := client.DefaultOption
	option.SerializeType = protocol.ProtoBuffer

	d := client.NewPeer2PeerDiscovery(&quot;tcp@&quot;+*addr, &quot;&quot;)
	xclient := client.NewXClient(&quot;ServiceUser&quot;, client.Failover, client.RandomSelect, d, client.DefaultOption)
	defer xclient.Close()

	args1 := &amp;amp;pb.ProtoArgs{
		A: 10,
		B: 20,
	}

	reply1 := &amp;amp;pb.ProtoReply{}
	err := xclient.Call(context.Background(), &quot;Mul&quot;, args1, reply1)

	args := &amp;amp;pb.Args{
		Id: 999,
	}

	reply := &amp;amp;pb.Reply{}

	err = xclient.Call(context.Background(), &quot;UserInfo&quot;, args, reply)

	if err != nil {
		log.Fatalf(&quot;failed to call: %v&quot;, err)
	}
	log.Printf(&quot;%d * %d = %d&quot;, args1.A, args1.B, reply1.C)
	log.Println(args.Id, &quot;:&quot;, reply)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来运行服务端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run server.go
2019/08/17 23:05:22 server.go:174: INFO : server pid:7124
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后运行客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
2019/08/17 23:05:26 10 * 20 = 200
2019/08/17 23:05:26 999 : UID:999 AddTime:14990093 UserType:2 Uface:&quot;http://image.xxxx.xxx/t.gif&quot; UserName:&quot;Joke&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很明显新设置的protocol.ProtoBuffer编解码器生效了。由于ProtoBuf使用上更加麻烦，而且和MsgPack相比反倒是MsgPack更有优势，所以在rpcx中默认使用MsgPack也就有了很好的理由。&lt;/p&gt;
&lt;p&gt;在rpcx中还可以定制新的编解码器，下面以gob作为新的编解码器，新建服务membergob来实验一下。&lt;/p&gt;
&lt;p&gt;首先修改service.go文件，在里面加入gob编解码两个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type GobCodec struct {
}

func (c *GobCodec) Decode(data []byte, i interface{}) error {
	enc := gob.NewDecoder(bytes.NewBuffer(data))
	err := enc.Decode(i)
	return err
}

func (c *GobCodec) Encode(i interface{}) ([]byte, error) {
	var buf bytes.Buffer
	enc := gob.NewEncoder(&amp;amp;buf)
	err := enc.Encode(i)
	return buf.Bytes(), err
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在服务端server.go文件中增加新的gob编解码器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;share.Codecs[protocol.SerializeType(5)] = &amp;amp;service.GobCodec{}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在客户端client.go文件中增加新的gob编解码器，同时修改Option选项中的SerializeType为新增的gob编解码器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;share.Codecs[protocol.SerializeType(5)] = &amp;amp;service.GobCodec{}

option := client.DefaultOption
option.SerializeType = protocol.SerializeType(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在可以运行服务端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run server.go
2019/08/17 21:42:57 server.go:174: INFO : server pid:2588
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后运行客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
2019/08/17 21:44:48 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而此时服务端控制台显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service: 999
2019/08/17 21:47:50 server.go:358: INFO : client has closed this connection: 127.0.0.1:62004
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过抓包可以看到端口62004与8972之间存在通信，如下图6所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/rpcx-6.png&quot; alt=&quot;rpcx-6.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图6 gob编解码&lt;/p&gt;
&lt;p&gt;如果需要新增其他的编解码器，只需要先实现编解码器，主要实现了Encode和Decode方法，就相当于有了新的编解码器，把新的编解码加入到rpcx中就非常容易了。所以，有兴趣的话读者可以自己尝试一下。&lt;/p&gt;
&lt;h2&gt;超时设置&lt;/h2&gt;
&lt;p&gt;随着对rpcx框架有了更多的了解，现在开始对框架进行更深一些入细致的了解，更多的细节弄清楚有助于我们更全面了解rpcx框架。比如在客户端和服务端，可以设置超时。&lt;/p&gt;
&lt;p&gt;超时机制可认为是一种保护机制，避免服务陷入无限的等待之中。在给定的时间没有响应则服务调用就进入下一个状态，或者重试、或者立即返回错误。&lt;/p&gt;
&lt;p&gt;在服务端，主要通过OptionFn来设置两种超时，分别是读超时readTimeout和写超时writeTimeout：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func WithReadTimeout(readTimeout time.Duration) OptionFn
func WithWriteTimeout(writeTimeout time.Duration) OptionFn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;既可以在实例化服务时使用NewServer(options ...OptionFn)，也可以直接使用WithReadTimeout()等函数来直接设置。&lt;/p&gt;
&lt;p&gt;在客户端可在Option中设置几个超时值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Option struct {
    ……
    // 连接超时
    ConnectTimeout time.Duration
    // 读超时
    ReadTimeout time.Duration
    // 写超时
    WriteTimeout time.Duration
    ……
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在DefaultOption 中设置了连接超时值为 10 秒，但并没有设置 ReadTimeout 和 WriteTimeout。没有设置的超时字段，可以根据情况来设置，但一般默认就可以了。&lt;/p&gt;
&lt;p&gt;在客户端中，使用context.Context也可以来控制超时，如使用context.WithTimeout 来设置超时时间，这是在客户端推荐的一种设置超时方式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;元数据&lt;/h2&gt;
&lt;p&gt;在上面的远程RPC调用中，远程调用的方法可以通过参数来传递数据。在rpcx中，客户端和服务端可以互相传递元数据。元数据指的也具体业务无关的基础数据。在rpcx中元数据是一个键值队的列表，键和值都是字符串。&lt;/p&gt;
&lt;p&gt;在服务器端注册服务的方法中有个设置元数据的参数metadata：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (s *Server) RegisterName(name string, rcvr interface{}, metadata string) error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数metadata一般为空字符串，但可以为服务增加一些元数据。比如服务状态（state）就是其中一类元数据，如果在元数据中设置了state=inactive，客户端将不能访问这些服务。这可以帮忙程序员临时禁用一些服务，大致实现服务的降级。&lt;/p&gt;
&lt;p&gt;下面实际来看看具体的例子，基于前面的用户服务来实现，新建服务membermeta，在cmd目录中建立state目录，该目录下分别建立server和client目录。大致结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;membermeta
├─cmd
│  └─state
│      ├─client
│      │      client.go
│      │
│      └─server
│              server.go
│
├─model
│      member.go
│
└─service
        service.go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改server.go文件，在RegisterName()方法中设置元数据state=inactive：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s.RegisterName(&quot;ServiceUser&quot;, new(service.ServiceUser), &quot;state=inactive&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改client.go文件中代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;d := client.NewMultipleServersDiscovery([]*client.KVPair{{Key: *addr, Value: &quot;state=inactive&quot;}, {Key: *addr1, Value: &quot;state=inactive&quot;}})	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行服务端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run server.go
2019/08/18 22:18:01 server.go:174: INFO : server pid:17696
2019/08/18 22:18:01 server.go:174: INFO : server pid:17696
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后运行客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
2019/08/18 22:18:06 connection.go:91: WARN : failed to dial server: dial tcp: missing address
2019/08/18 22:18:06 connection.go:91: WARN : failed to dial server: dial tcp: missing address
2019/08/18 22:18:06 connection.go:91: WARN : failed to dial server: dial tcp: missing address
2019/08/18 22:18:06 failed to call: can not found any server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到客户都已经不能正常与服务端通信了，说明设置元数据state=inactive生效。这种通过设置元数据来修改服务状态的方法，在客户端如果不设置，即使服务端设置了也不会生效。&lt;/p&gt;
&lt;p&gt;例如把客户端分别修改为下面三种情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;d := client.NewMultipleServersDiscovery([]*client.KVPair{{Key: *addr}, {Key: *addr1}})	
d := client.NewMultipleServersDiscovery([]*client.KVPair{{Key: *addr}, {Key: *addr1, Value: &quot;state=inactive&quot;}})	
d := client.NewMultipleServersDiscovery([]*client.KVPair{{Key: *addr, Value: &quot;state=inactive&quot;}, {Key: *addr1}})	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在服务端不改变情况下，分别运行三次客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
=== tcp@localhost:8972
2019/08/18 22:23:38 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
=== tcp@localhost:8972
2019/08/18 22:23:55 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
=== tcp@localhost:8973
2019/08/18 22:24:13 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三种情况很明显，第一个服务器服务状态为inactive，只有第二个服务器正常，所以由它提供服务。&lt;/p&gt;
&lt;p&gt;而甚至如果服务端不设置元数据state=inactive，客户端设置了元数据state=inactive也依然生效。如服务端不设置元数据state=inactive，而客户端设置为：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;d := client.NewMultipleServersDiscovery([]*client.KVPair{{Key: *addr, Value: &quot;state=inactive&quot;}, {Key: *addr1, Value: &quot;state=inactive&quot;}})&lt;/code&gt;
则客户端运行结果依然是能正常与服务端通信。虽然服务端可不用设置，但建议还是设置这个参数。但如果客户端不设置这个元数据，则服务端无论怎样设置都不会起作用，这个必须要注意。&lt;/p&gt;
&lt;p&gt;和服务状态类似的还有分组（Group）元数据，在初始化客户端实例时，NewXClient()函数中会调用下面的函数filterByStateAndGroup()，这个函数会检查服务状态和分组两个元数据信息，根据情况来把对应服务器从列表中删除：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func filterByStateAndGroup(group string, servers map[string]string) {
	for k, v := range servers {
		if values, err := url.ParseQuery(v); err == nil {
			if state := values.Get(&quot;state&quot;); state == &quot;inactive&quot; {
				delete(servers, k)
			}
			if group != &quot;&quot; &amp;amp;&amp;amp; group != values.Get(&quot;group&quot;) {
				delete(servers, k)
			}
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了测试分组元数据的使用，在上面的服务membermeta的cmd目录中新建group目录，分别建立server目录和client目录。&lt;/p&gt;
&lt;p&gt;对server.go做简单修改，仅仅只添加metadata参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s.RegisterName(&quot;ServiceUser&quot;, new(service.ServiceUser), &quot;group=Member&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对client.go文件主要修改如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	......
d := client.NewMultipleServersDiscovery([]*client.KVPair{{Key: *addr, Value: &quot;&quot;}, {Key: *addr1, Value: &quot;group=Member&quot;}})
	option := client.DefaultOption
	option.Group = &quot;Me&quot;
	xclient := client.NewXClient(&quot;ServiceUser&quot;, client.Failover, client.RoundRobin, d, option)
	defer xclient.Close()
......
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行服务端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run server.go
2019/08/19 00:02:06 server.go:174: INFO : server pid:7400
2019/08/19 00:02:06 server.go:174: INFO : server pid:7400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后运行客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
2019/08/19 00:14:06 failed to call: can not found any server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于在服务端设置的元数据“group=Member”，而在客户端设置的option.Group = &quot;Me&quot;，客户端和服务端不在一组，所以客户端不能访问到服务器。&lt;/p&gt;
&lt;p&gt;此时再修改下client.go文件，修改客户端设置的分组为服务端设置的分组值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;option.Group = &quot;Member&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再运行客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
=== tcp@localhost:8973
2019/08/19 00:21:38 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和预想中基本一样，由于服务端和客户端都设置在同一分组，而且在服务发现中指定8973端口的服务器为分组Member，所以应该只有这台服务器可以访问，而且运行结果也证实了这一点。&lt;/p&gt;
&lt;p&gt;如果此时把服务发现的分组元数据指定为其他值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;d := client.NewMultipleServersDiscovery([]*client.KVPair{{Key: *addr, Value: &quot;&quot;}, {Key: *addr1, Value: &quot;group=009&quot;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则客户端运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
2019/08/19 00:22:52 failed to call: can not found any server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，运行结果失败，原因是由于该分组没有服务器，所以调用服务失败。所以如果在服务发现中指定分组值，与服务端也需要保持一致。&lt;/p&gt;
&lt;p&gt;和服务状态一样，分组也可以在客户端避开。如果在客户端不设置Group这个可选项，其实分组的限制是不起作用的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;option := client.DefaultOption
option.Group = &quot;Member&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把客户端的这个设置取消掉，改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;option := client.DefaultOption
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再运行客户端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go run client.go
=== tcp@localhost:8972
2019/08/19 00:33:08 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然服务发现中指定的元数据和服务端不一样，但是由于option.Group 没有设置，分组的限制没有生效。&lt;/p&gt;
&lt;h2&gt;网关&lt;/h2&gt;
&lt;p&gt;在rpcx框架中，可以通过网关（Gateway）的方式来实现跨语言的调用，比如Java、Python、C#、Node.js、Php、C\C++、Rust等来调用 rpcx 服务。如图7所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/rpcx-7.png&quot; alt=&quot;rpcx-7.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图7 网关&lt;/p&gt;
&lt;p&gt;使用网关程序有两种部署模式Gateway和Agent。&lt;/p&gt;
&lt;p&gt;1.Gateway：网关模式需要将网关程序独立部署，所有http请求都将先发送给网关，网关将其转换为rpcx请求，再来调用相关rpcx服务，并将rpcx的返回结果转换成http响应, 最终返回给用户。&lt;/p&gt;
&lt;p&gt;2.Agent：代理模式是将网关程序和客户端程序一起部署，代理作为一个后台服务部署。客户端发送http请求到本地的代理, 本地的代理将请求转为rpcx请求，并转发到相应的rpcx服务，最后将rpcx的返回结果转换为http响应返回给客户端，类似于Istio中的Sidecar。&lt;/p&gt;
&lt;p&gt;下面来实际演示一下网关，在发送http请求时，需要额外设置一些Header信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    X-RPCX-Version: rpcx 版本
    X-RPCX-MesssageType: 设置为0,代表请求
    X-RPCX-Heartbeat: 是否是心跳请求, 缺省false
    X-RPCX-Oneway: 是否是单向请求, 缺省false.
    X-RPCX-SerializeType: 0 as raw bytes, 1 as JSON, 2 as protobuf, 3 as msgpack
    X-RPCX-MessageID: 消息id, uint64 类型
    X-RPCX-ServicePath: service path
    X-RPCX-ServiceMethod: service method
    X-RPCX-Meta: 额外的元数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而对于 http 响应，也有对应的Header信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    X-RPCX-Version: rpcx 版本
    X-RPCX-MesssageType: 1 ,代表响应
    X-RPCX-Heartbeat: 是否是heartbeat请求
    X-RPCX-MessageStatusType: Error 还是正常返回结果
    X-RPCX-SerializeType: 0 as raw bytes, 1 as JSON, 2 as protobuf, 3 as msgpack
    X-RPCX-MessageID: 消息id, uint64 类型
    X-RPCX-ServicePath: service path
    X-RPCX-ServiceMethod: service method
    X-RPCX-Meta: 额外的元数据
    X-RPCX-ErrorMessage: 错误信息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在用户服务的基础上来看看网关的实际运行情况，在用户服务目录下cmd目录中新建gateway目录，作为测试目录。&lt;/p&gt;
&lt;p&gt;在client目录中增加一个文件httpclient.go，通过这个文件来测试网关连接rpcx服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
	cc := &amp;amp;codec.MsgpackCodec{}

	args := service.Args{
		Uid: 999,
	}
	data, _ := cc.Encode(args)

	req, err := http.NewRequest(&quot;POST&quot;, &quot;http://127.0.0.1:8972/&quot;, bytes.NewReader(data))
	if err != nil {
		log.Fatal(&quot;failed to create request: &quot;, err)
		return
	}

	h := req.Header
	h.Set(gateway.XMessageID, &quot;10000&quot;)
	h.Set(gateway.XMessageType, &quot;0&quot;)
	h.Set(gateway.XSerializeType, &quot;3&quot;)
	h.Set(gateway.XServicePath, &quot;ServiceUser&quot;)
	h.Set(gateway.XServiceMethod, &quot;UserInfo&quot;)

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(&quot;failed to call: &quot;, err)
	}
	defer res.Body.Close()

	// handle http response
	replyData, err := ioutil.ReadAll(res.Body)
	if err != nil {
		log.Fatal(&quot;failed to read response: &quot;, err)
	}

	reply := &amp;amp;service.Reply{}
	err = cc.Decode(replyData, reply)
	if err != nil {
		log.Fatal(&quot;failed to decode reply: &quot;, err)
	}

	log.Println(args.Uid, &quot;:&quot;, reply.User)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行服务端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run server.go
2019/08/19 20:53:51 server.go:174: INFO : server pid:16136
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后运行含有网关的客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run httpclient.go
2019/08/19 21:02:43 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到http响应正常。&lt;/p&gt;
&lt;h2&gt;断路器（Circuit Breaker）&lt;/h2&gt;
&lt;p&gt;在rpcx中，可能出现一个节点宕机的情况，可以使用断路器（Circuit Breaker）模式来避免这个错误影响到其他服务，防止出现雪崩情况。&lt;/p&gt;
&lt;p&gt;客户端通过断路器调用服务， 一旦连续的错误达到一个阈值，断路器就会断开进行保护，如果继续调用这个节点，系统直接返回错误。经过一段时间，断路器会处于半开的状态，允许一定数量的请求尝试发送到这个节点，如果正常访问，断路器就处于全开的状态，否则又进入短路的状态。&lt;/p&gt;
&lt;p&gt;在rpcx 这样定义了断路器 Breaker 接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Breaker is a CircuitBreaker interface.
type Breaker interface {
	Call(func() error, time.Duration) error
	Fail()
	Success()
	Ready() bool
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在rpcx 中只提供了一个简单的断路器 ConsecCircuitBreaker，实现代码保存在circuit_breaker.go文件中，它在连续发生规定数量的故障或超时后跳闸，再经过一段时间后打开。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;option := client.DefaultOption
option.GenBreaker = func() client.Breaker { return client.NewConsecCircuitBreaker(5, 30*time.Second) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是在用户服务的cmd目录中建立breaker目录来演示断路器在rpcx中的作用。通过在服务端两个端口只启动一个服务的简单模拟故障发生：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var (
	addr0 = flag.String(&quot;addr0&quot;, &quot;localhost:8972&quot;, &quot;server0 address&quot;)
	addr1 = flag.String(&quot;addr1&quot;, &quot;localhost:8973&quot;, &quot;server1 address&quot;)
)

func main() {
	flag.Parse()

	go createServer(*addr0)
	//go createServer(*addr1)

	select {}
}

func createServer(addr string) {
	s := server.NewServer()

	s.RegisterName(&quot;ServiceUser&quot;, new(service.ServiceUser), &quot;&quot;)
	err := s.Serve(&quot;tcp&quot;, addr)
	if err != nil {
		panic(err)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端需要在Option中指定GenBreaker，这里指定rpcx自带的断路器，这个断路器只有一种触发的条件，即连续多次触发断路，这里设定为2次：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var (
	addr  = flag.String(&quot;addr&quot;, &quot;localhost:8972&quot;, &quot;server address&quot;)
	addr1 = flag.String(&quot;addr1&quot;, &quot;localhost:8973&quot;, &quot;server1 address&quot;)
)

func main() {
	flag.Parse()

	option := client.DefaultOption
	option.GenBreaker = func() client.Breaker { return client.NewConsecCircuitBreaker(2, 30*time.Second) }

	d := client.NewMultipleServersDiscovery([]*client.KVPair{{Key: *addr}, {Key: *addr1}})
	option.Retries = 5
	xclient := client.NewXClient(&quot;ServiceUser&quot;, client.Failtry, client.RandomSelect, d, option)
	defer xclient.Close()

	args := service.Args{
		Uid: 999,
	}
	for i := 0; i &amp;lt; 100; i++ {
		reply := &amp;amp;service.Reply{}
		err := xclient.Call(context.Background(), &quot;UserInfo&quot;, args, reply)
		if err != nil {
			log.Printf(&quot;failed to call: %v&quot;, err)
		}

		log.Println(args.Uid, &quot;:&quot;, reply.User)
		time.Sleep(time.Second)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行服务端程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run server.go
2019/08/20 16:51:30 server.go:174: INFO : server pid:20496
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后运行含有断路器的客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
=== localhost:8973
2019/08/20 18:29:42 connection.go:91: WARN : failed to dial server: dial tcp [::1]:8973: connectex: No connection could be made because the target machine actively refused it.
2019/08/20 18:29:43 connection.go:91: WARN : failed to dial server: dial tcp [::1]:8973: connectex: No connection could be made because the target machine actively refused it.
2019/08/20 18:29:43 failed to call: dial tcp [::1]:8973: connectex: No connection could be made because the target machine actively refused it.
2019/08/20 18:29:43 999 : {0 0 0  }
=== localhost:8973
2019/08/20 18:29:44 failed to call: breaker open
2019/08/20 18:29:44 999 : {0 0 0  }
=== localhost:8973
2019/08/20 18:29:45 failed to call: breaker open
2019/08/20 18:29:45 999 : {0 0 0  }
=== localhost:8972
2019/08/20 18:29:46 999 : {999 14990093 2 http://image.xxxx.xxx/t.gif Joke}
=== localhost:8973
2019/08/20 18:29:47 failed to call: breaker open
2019/08/20 18:29:47 999 : {0 0 0  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端设置的失败模式是Failtry，所以重试两次后触发断路器，在断路器生效期间，再次调用则显示breaker open，表明断路器在有效期，这期间不会继续调用已经出问题的服务， 从而达到保护的目的，整个系统不会出现因为超时而产生的雪崩式连锁反应。&lt;/p&gt;
&lt;p&gt;另外有开源包：github.com/rubyist/circuitbreaker，提供更多的断路器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func NewBreaker() *Breaker                         // 空断路器
func NewThresholdBreaker(threshold int64) *Breaker     // 失败次数
func NewConsecutiveBreaker(threshold int64) *Breaker   // 连续失败次数
func NewRateBreaker(rate float64, minSamples int64) *Breaker // 根据失败比率
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把客户端代码稍微修改下，改为使用circuitbreaker来做断路器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;option := client.DefaultOption
	option.GenBreaker = func() client.Breaker {
		return circuit.NewBreakerWithOptions(&amp;amp;circuit.Options{
			ShouldTrip: circuit.ThresholdTripFunc(2),
			WindowTime: 30 * time.Second,
		})
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行客户端，也可以实现断路器模式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;go run client.go
......
2019/08/20 19:30:06 failed to call: dial tcp [::1]:8973: connectex: No connection could be made because the target machine actively refused it.
2019/08/20 19:30:06 999 : {0 0 0  }
=== localhost:8973
2019/08/20 19:30:07 failed to call: breaker open
2019/08/20 19:30:07 999 : {0 0 0  }
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第四十二章 WEB框架(Gin)</title><link>https://blog.wemang.com/posts/go/study/42_42_gin/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_42_gin/</guid><pubDate>Fri, 23 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;42.1 有关于Gin&lt;/h2&gt;
&lt;p&gt;在Go语言开发的WEB框架中，有两款著名WEB框架的命名都以酒有关：Martini（ 马丁尼）和Gin（杜松子酒），由于我不擅于饮酒所以这两种酒的优劣暂不做评价，但说WEB框架相比较的话，Gin要比Martini强很多。&lt;/p&gt;
&lt;p&gt;Gin是Go语言写的一个WEB框架，它具有运行速度快，分组的路由器，良好的崩溃捕获和错误处理，非常好的支持中间件和JSON。总之在Go语言开发领域是一款值得好好研究的WEB框架，开源网址：https://github.com/gin-gonic/gin&lt;/p&gt;
&lt;p&gt;首先下载安装gin包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go get -u github.com/gin-gonic/gin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个简单的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;github.com/gin-gonic/gin&quot;

func main() {
	r := gin.Default()
	r.GET(&quot;/ping&quot;, func(c *gin.Context) {
		c.JSON(200, gin.H{
			&quot;message&quot;: &quot;pong&quot;, 
		})
	})
	r.Run() // listen and serve on 0.0.0.0:8080
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译运行程序，打开浏览器，访问 http://localhost:8080/ping&lt;br /&gt;
页面显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;message&quot;:&quot;pong&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以JSON格式输出了数据。&lt;/p&gt;
&lt;p&gt;gin的功能不只是简单输出JSON数据。它是一个轻量级的WEB框架，支持RESTful风格API，支持GET，POST，PUT，PATCH，DELETE，OPTIONS 等http方法，支持文件上传，分组路由，Multipart/Urlencoded FORM，以及支持JSONP，参数处理等等功能，这些都和WEB紧密相关，通过提供这些功能，使开发人员更方便地处理WEB业务。&lt;/p&gt;
&lt;h2&gt;42.2 Gin实际应用&lt;/h2&gt;
&lt;p&gt;接下来使用Gin作为框架来搭建一个拥有静态资源站点，动态WEB站点，以及RESTFull API接口站点（可专门作为手机APP应用提供服务使用）组成的，亦可根据情况分拆这套系统，每种功能独立出来单独提供服务。&lt;/p&gt;
&lt;p&gt;下面按照一套系统但采用分站点来说明，首先是整个系统的目录结构，website目录下面static是资源类文件，为静态资源站点专用；photo目录是UGC上传图片目录，tpl是动态站点的模板。当然这个目录结构是一种约定，你可以根据情况来修改。整个项目已经开源，你可以访问来详细了解：https://github.com/ffhelicopter/tmm&lt;/p&gt;
&lt;p&gt;具体每个站点的功能怎么实现呢？请看下面有关每个功能的讲述：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一：静态资源站点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一般网站开发中，我们会考虑把js，css，以及资源图片放在一起，作为静态站点部署在CDN，提升响应速度。采用Gin实现起来非常简单，当然也可以使用net/http包轻松实现，但使用Gin会更方便。&lt;/p&gt;
&lt;p&gt;不管怎么样，使用Go开发，我们可以不用花太多时间在WEB服务环境搭建上，程序启动就直接可以提供WEB服务了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;net/http&quot;
	&quot;github.com/gin-gonic/gin&quot;
)

func main() {
	router := gin.Default()

	// 静态资源加载，本例为css,js以及资源图片
	router.StaticFS(&quot;/public&quot;, http.Dir(&quot;D:/goproject/src/github.com/ffhelicopter/tmm/website/static&quot;))
	router.StaticFile(&quot;/favicon.ico&quot;, &quot;./resources/favicon.ico&quot;)

	// Listen and serve on 0.0.0.0:80
	router.Run(&quot;:80&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先需要是生成一个Engine ，这是gin的核心，默认带有Logger 和 Recovery 两个中间件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;router := gin.Default()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;StaticFile 是加载单个文件，而StaticFS 是加载一个完整的目录资源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些目录下资源是可以随时更新，而不用重新启动程序。现在编译运行程序，静态站点就可以正常访问了。&lt;/p&gt;
&lt;p&gt;访问http://localhost/public/images/logo.jpg 图片加载正常。每次请求响应都会在服务端有日志产生，包括响应时间，加载资源名称，响应状态值等等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二：动态站点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果需要动态交互的功能，比如发一段文字+图片上传。由于这些功能出来前端页面外，还需要服务端程序一起来实现，而且迭代需要经常需要修改代码和模板，所以把这些统一放在一个大目录下，姑且称动态站点。&lt;/p&gt;
&lt;p&gt;tpl是动态站点所有模板的根目录，这些模板可调用静态资源站点的css，图片等；photo是图片上传后存放的目录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;context&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;os&quot;
	&quot;os/signal&quot;
	&quot;time&quot;

	&quot;github.com/ffhelicopter/tmm/handler&quot;

	&quot;github.com/gin-gonic/gin&quot;
)

func main() {
	router := gin.Default()

	// 静态资源加载，本例为css,js以及资源图片
	router.StaticFS(&quot;/public&quot;, http.Dir(&quot;D:/goproject/src/github.com/ffhelicopter/tmm/website/static&quot;))
	router.StaticFile(&quot;/favicon.ico&quot;, &quot;./resources/favicon.ico&quot;)

	// 导入所有模板，多级目录结构需要这样写
	router.LoadHTMLGlob(&quot;website/tpl/*/*&quot;)

	// website分组
	v := router.Group(&quot;/&quot;)
	{

		v.GET(&quot;/index.html&quot;, handler.IndexHandler)
		v.GET(&quot;/add.html&quot;, handler.AddHandler)
		v.POST(&quot;/postme.html&quot;, handler.PostmeHandler)
	}

	// router.Run(&quot;:80&quot;) 
	// 这样写就可以了，下面所有代码（go1.8+）是为了优雅处理重启等动作。
	srv := &amp;amp;http.Server{
		Addr:         &quot;:80&quot;,
		Handler:      router,
		ReadTimeout:  30 * time.Second,
		WriteTimeout: 30 * time.Second,
	}

	go func() {
		// 监听请求
		if err := srv.ListenAndServe(); err != nil &amp;amp;&amp;amp; err != http.ErrServerClosed {
			log.Fatalf(&quot;listen: %s\n&quot;, err)
		}
	}()

	// 优雅Shutdown（或重启）服务
	quit := make(chan os.Signal)
	signal.Notify(quit, os.Interrupt) // syscall.SIGKILL
	&amp;lt;-quit
	log.Println(&quot;Shutdown Server ...&quot;)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal(&quot;Server Shutdown:&quot;, err)
	}
	select {
	case &amp;lt;-ctx.Done():
	}	
	log.Println(&quot;Server exiting&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在动态站点实现中，引入WEB分组以及优雅重启这两个功能。WEB分组功能可以通过不同的入口根路径来区别不同的模块，这里我们可以访问：http://localhost/index.html 。如果新增一个分组，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;v := router.Group(&quot;/login&quot;) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以访问：http://localhost/login/xxxx ，xxx是我们在v.GET方法或v.POST方法中的路径。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	// 导入所有模板，多级目录结构需要这样写
	router.LoadHTMLGlob(&quot;website/tpl/*/*&quot;)
	
	// website分组
	v := router.Group(&quot;/&quot;)
	{

		v.GET(&quot;/index.html&quot;, handler.IndexHandler)
		v.GET(&quot;/add.html&quot;, handler.AddHandler)
		v.POST(&quot;/postme.html&quot;, handler.PostmeHandler)
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过router.LoadHTMLGlob(&quot;website/tpl/&lt;em&gt;/&lt;/em&gt;&quot;) 导入模板根目录下所有的文件。在前面有讲过html/template 包的使用，这里模板文件中的语法和前面一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	router.LoadHTMLGlob(&quot;website/tpl/*/*&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如v.GET(&quot;/index.html&quot;, handler.IndexHandler) ，通过访问http://localhost/index.html 这个URL，实际由handler.IndexHandler来处理。而在tmm目录下的handler存放了package handler 文件。在包里定义了IndexHandler函数，它使用了index.html模板。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func IndexHandler(c *gin.Context) {
	c.HTML(http.StatusOK, &quot;index.html&quot;, gin.H{
		&quot;Title&quot;: &quot;作品欣赏&quot;,
	})
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;index.html模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
{{template &quot;header&quot; .\}\}
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;!--导航--&amp;gt;
&amp;lt;div class=&quot;feeds&quot;&amp;gt;
    &amp;lt;div class=&quot;top-nav&quot;&amp;gt;
    	&amp;lt;a href=&quot;/index.tml&quot; class=&quot;active&quot;&amp;gt;欣赏&amp;lt;/a&amp;gt;
    	&amp;lt;a href=&quot;/add.html&quot; class=&quot;add-btn&quot;&amp;gt;
    		&amp;lt;svg class=&quot;icon&quot; aria-hidden=&quot;true&quot;&amp;gt;
    			&amp;lt;use  xlink:href=&quot;#icon-add&quot;&amp;gt;&amp;lt;/use&amp;gt;
    		&amp;lt;/svg&amp;gt;
    		发布
    	&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
	&amp;lt;input type=&quot;hidden&quot; id=&quot;showmore&quot; value=&quot;{$showmore}&quot;&amp;gt;
	&amp;lt;input type=&quot;hidden&quot; id=&quot;page&quot; value=&quot;{$page}&quot;&amp;gt;
    &amp;lt;!--&amp;lt;/div&amp;gt;--&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;script type=&quot;text/javascript&quot;&amp;gt;
	var done = true;
	$(window).scroll(function(){
        var scrollTop = $(window).scrollTop();
        var scrollHeight = $(document).height();
        var windowHeight = $(window).height();
        var showmore = $(&quot;#showmore&quot;).val();
        if(scrollTop + windowHeight + 300 &amp;gt;= scrollHeight &amp;amp;&amp;amp; showmore == 1 &amp;amp;&amp;amp; done){
        	var page = $(&quot;#page&quot;).val();
        	done = false;
	        $.get(&quot;{:U(&apos;Product/listsAjax&apos;)}&quot;, { page : page }, function(json) {
	        	if (json.rs != &quot;&quot;) {
	        		$(&quot;.feeds&quot;).append(json.rs);
	        		$(&quot;#showmore&quot;).val(json.showmore);
	        		$(&quot;#page&quot;).val(json.page);
	        		done = true;
	        	}
	        },&apos;json&apos;);
        }
    });
&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;//at.alicdn.com/t/font_ttszo9rnm0wwmi.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在index.html模板中，通过&lt;code&gt;{{template &quot;header&quot; }}&lt;/code&gt;语句，嵌套了header.html模板。&lt;/p&gt;
&lt;p&gt;header.html模板：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{ define &quot;header&quot; \}\}
	&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;	
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, minimal-ui&quot;&amp;gt;
	&amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge,chrome=1&quot;&amp;gt;
	&amp;lt;meta name=&quot;format-detection&quot; content=&quot;telephone=no,email=no&quot;&amp;gt;
	&amp;lt;title&amp;gt;{{ .Title \}\}&amp;lt;/title&amp;gt;
	&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;/public/css/common.css&quot;&amp;gt;
	&amp;lt;script src=&quot;/public/lib/jquery-3.1.1.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;/public/lib/jquery.cookie.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;link href=&quot;/public/css/font-awesome.css?v=4.4.0&quot; rel=&quot;stylesheet&quot;&amp;gt;
{{ end \}\}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;{{ define &quot;header&quot; }}&lt;/code&gt; 让我们在模板嵌套时直接使用header名字，而在index.html中的&lt;code&gt;{{template &quot;header&quot; }}&lt;/code&gt; 注意“.”，可以使参数嵌套传递，否则不能传递，比如这里的Title。&lt;/p&gt;
&lt;p&gt;现在我们访问 &lt;code&gt;http://localhost/index.html&lt;/code&gt;  ，可以看到浏览器显示Title 是“作品欣赏”，这个Title是通过IndexHandler来指定的。&lt;/p&gt;
&lt;p&gt;接下来点击“发布”按钮，我们进入发布页面，上传图片，点击“完成”提交，会提示我们成功上传图片。可以在photo目录中看到刚才上传的图片。&lt;/p&gt;
&lt;p&gt;注意：
由于在本人在发布到github的代码中，在处理图片上传的代码中，除了服务器存储外，还实现了IPFS发布存储，如果不需要IPFS，请注释相关代码。&lt;/p&gt;
&lt;p&gt;有关IPFS:
IPFS本质上是一种内容可寻址、版本化、点对点超媒体的分布式存储、传输协议，目标是补充甚至取代过去20年里使用的超文本媒体传输协议（HTTP），希望构建更快、更安全、更自由的互联网时代。&lt;/p&gt;
&lt;p&gt;IPFS 不算严格意义上区块链项目，是一个去中心化存储解决方案，但有些区块链项目通过它来做存储。&lt;/p&gt;
&lt;p&gt;IPFS项目有在github上开源，Go语言实现哦，可以关注并了解。&lt;/p&gt;
&lt;p&gt;优雅重启在迭代中有较好的实际意义，每次版本发布，如果直接停服务在部署重启，对业务还是有蛮大的影响，而通过优雅重启，这方面的体验可以做得更好些。这里ctrl + c 后过5秒服务停止。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三：中间件的使用，在API中可能使用限流，身份验证等&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Go 语言中net/http设计的一大特点就是特别容易构建中间件。 gin也提供了类似的中间件。需要注意的是在gin里面中间件只对注册过的路由函数起作用。&lt;/p&gt;
&lt;p&gt;而对于分组路由，嵌套使用中间件，可以限定中间件的作用范围。大致分为全局中间件，单个路由中间件和分组中间件。&lt;/p&gt;
&lt;p&gt;即使是全局中间件，其使用前的代码不受影响。 也可在handler中局部使用，具体见api.GetUser 。&lt;/p&gt;
&lt;p&gt;在高并发场景中，有时候需要用到限流降速的功能，这里引入一个限流中间件。有关限流方法常见有两种，具体可自行研究，这里只讲使用。&lt;/p&gt;
&lt;p&gt;导入 import  &quot;github.com/didip/tollbooth/limiter&quot; 包，在上面代码基础上增加如下语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	//rate-limit 限流中间件 
	lmt := tollbooth.NewLimiter(1, nil)
	lmt.SetMessage(&quot;服务繁忙，请稍后再试...&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;v.GET(&quot;/index.html&quot;, LimitHandler(lmt), handler.IndexHandler) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当F5刷新刷新http://localhost/index.html 页面时，浏览器会显示：服务繁忙，请稍后再试...&lt;/p&gt;
&lt;p&gt;限流策略也可以为IP：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tollbooth.LimitByKeys(lmt, []string{&quot;127.0.0.1&quot;, &quot;/&quot;})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更多限流策略的配置，可以进一步github.com/didip/tollbooth/limiter 了解。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;四：RESTful API接口&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;前面说了在gin里面可以采用分组来组织访问URL，这里RESTful API需要给出不同的访问URL来和动态站点区分，所以新建了一个分组v1。&lt;/p&gt;
&lt;p&gt;在浏览器中访问http://localhost/v1/user/1100000/&lt;/p&gt;
&lt;p&gt;这里对v1.GET(&quot;/user/:id/*action&quot;, LimitHandler(lmt), api.GetUser) 进行了限流控制，所以如果频繁访问上面地址也将会有限制，这在API接口中非常有作用。&lt;/p&gt;
&lt;p&gt;通过 api这个包，来实现所有有关API的代码。在GetUser函数中，通过读取mysql数据库，查找到对应userid的用户信息，并通过JSON格式返回给client。&lt;/p&gt;
&lt;p&gt;在api.GetUser中，设置了一个局部中间件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	//CORS 局部CORS，可在路由中设置全局的CORS
	c.Writer.Header().Add(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;gin关于参数的处理，api包中api.go文件中有简单说明，限于篇幅原因，就不在此展开。这个项目的详细情况，请访问 https://github.com/ffhelicopter/tmm 了解。有关gin的更多信息，请访问 https://github.com/gin-gonic/gin ，该开源项目比较活跃，可以关注。&lt;/p&gt;
&lt;p&gt;完整main.go代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;context&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;os&quot;
	&quot;os/signal&quot;
	&quot;time&quot;

	&quot;github.com/didip/tollbooth&quot;
	&quot;github.com/didip/tollbooth/limiter&quot;
	&quot;github.com/ffhelicopter/tmm/api&quot;
	&quot;github.com/ffhelicopter/tmm/handler&quot;

	&quot;github.com/gin-gonic/gin&quot;
)

// 定义全局的CORS中间件
func Cors() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Writer.Header().Add(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;)
		c.Next()
	}
}

func LimitHandler(lmt *limiter.Limiter) gin.HandlerFunc {
	return func(c *gin.Context) {
		httpError := tollbooth.LimitByRequest(lmt, c.Writer, c.Request)
		if httpError != nil {
			c.Data(httpError.StatusCode, lmt.GetMessageContentType(), []byte(httpError.Message))
			c.Abort()
		} else {
			c.Next()
		}
	}
}

func main() {
gin.SetMode(gin.ReleaseMode)
	router := gin.Default()

	// 静态资源加载，本例为css,js以及资源图片
	router.StaticFS(&quot;/public&quot;, http.Dir(&quot;D:/goproject/src/github.com/ffhelicopter/tmm/website/static&quot;))
	router.StaticFile(&quot;/favicon.ico&quot;, &quot;./resources/favicon.ico&quot;)

	// 导入所有模板，多级目录结构需要这样写
	router.LoadHTMLGlob(&quot;website/tpl/*/*&quot;)
	// 也可以根据handler，实时导入模板。

	// website分组
	v := router.Group(&quot;/&quot;)
	{

		v.GET(&quot;/index.html&quot;, handler.IndexHandler)
		v.GET(&quot;/add.html&quot;, handler.AddHandler)
		v.POST(&quot;/postme.html&quot;, handler.PostmeHandler)
	}

	// 中间件 golang的net/http设计的一大特点就是特别容易构建中间件。
	// gin也提供了类似的中间件。需要注意的是中间件只对注册过的路由函数起作用。
	// 对于分组路由，嵌套使用中间件，可以限定中间件的作用范围。
	// 大致分为全局中间件，单个路由中间件和群组中间件。

	// 使用全局CORS中间件。
	// router.Use(Cors())
	// 即使是全局中间件，在use前的代码不受影响
	// 也可在handler中局部使用，见api.GetUser

	//rate-limit 中间件
	lmt := tollbooth.NewLimiter(1, nil)
	lmt.SetMessage(&quot;服务繁忙，请稍后再试...&quot;)

	// API分组(RESTFULL)以及版本控制
	v1 := router.Group(&quot;/v1&quot;)
	{
		// 下面是群组中间的用法
		// v1.Use(Cors())

		// 单个中间件的用法
		// v1.GET(&quot;/user/:id/*action&quot;,Cors(), api.GetUser)

		// rate-limit
		v1.GET(&quot;/user/:id/*action&quot;, LimitHandler(lmt), api.GetUser)

		//v1.GET(&quot;/user/:id/*action&quot;, Cors(), api.GetUser)
		// AJAX OPTIONS ，下面是有关OPTIONS用法的示例
		// v1.OPTIONS(&quot;/users&quot;, OptionsUser)      // POST
		// v1.OPTIONS(&quot;/users/:id&quot;, OptionsUser)  // PUT, DELETE
	}

	srv := &amp;amp;http.Server{
		Addr:         &quot;:80&quot;,
		Handler:      router,
		ReadTimeout:  30 * time.Second,
		WriteTimeout: 30 * time.Second,
	}

	go func() {
		if err := srv.ListenAndServe(); err != nil &amp;amp;&amp;amp; err != http.ErrServerClosed {
			log.Fatalf(&quot;listen: %s\n&quot;, err)
		}
	}()

	// 优雅Shutdown（或重启）服务
	// 5秒后优雅Shutdown服务
	quit := make(chan os.Signal)
	signal.Notify(quit, os.Interrupt) //syscall.SIGKILL
	&amp;lt;-quit
	log.Println(&quot;Shutdown Server ...&quot;)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal(&quot;Server Shutdown:&quot;, err)
	}
	select {
	case &amp;lt;-ctx.Done():
	}
	log.Println(&quot;Server exiting&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/README.md&quot;&gt;前言&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_41_crawler.md&quot;&gt;第四十一章 网络爬虫&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第四十一章 网络爬虫</title><link>https://blog.wemang.com/posts/go/study/42_41_crawler/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_41_crawler/</guid><pubDate>Thu, 22 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;41.1 Colly网络爬虫框架&lt;/h2&gt;
&lt;p&gt;Colly是用Go实现的网络爬虫框架。Colly快速优雅，在单核上每秒可以发起1K以上请求；以回调函数的形式提供了一组接口，可以实现任意类型的爬虫。&lt;/p&gt;
&lt;p&gt;Colly 特性：&lt;/p&gt;
&lt;p&gt;清晰的API
快速（单个内核上的请求数大于1k）
管理每个域的请求延迟和最大并发数
自动cookie 和会话处理
同步/异步/并行抓取
高速缓存
自动处理非Unicode的编码
支持Robots.txt
定制Agent信息
定制抓取频次&lt;/p&gt;
&lt;p&gt;特性如此多，引无数程序员竞折腰。下面开始我们的Colly之旅：&lt;/p&gt;
&lt;p&gt;首先，下载安装第三方包：go get -u github.com/gocolly/colly/...&lt;/p&gt;
&lt;p&gt;接下来在代码中导入包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &quot;github.com/gocolly/colly&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;准备工作已经完成，接下来就看看Colly的使用方法和主要的用途。&lt;/p&gt;
&lt;p&gt;colly的主体是Collector对象，管理网络通信和负责在作业运行时执行附加的回调函数。使用colly需要先初始化Collector：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c := colly.NewCollector() 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看看NewCollector，它也是变参函数，参数类型为函数类型func(*Collector)，主要是用来初始化一个&amp;amp;Collector{}对象。&lt;/p&gt;
&lt;p&gt;而在Colly中有好些函数都返回这个函数类型func(*Collector)，如UserAgent(us string)用来设置UA。所以，这里其实是一种初始化对象，设置对象属性的一种模式。相比使用方法（set方法）这种传统方式来初始设置对象属性，采用回调函数的形式在Go语言中更灵活更方便：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NewCollector(options ...func(*Collector)) *Collector
UserAgent(ua string) func(*Collector)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一旦得到一个colly对象，可以向colly附加各种不同类型的回调函数（回调函数在Colly中广泛使用），来控制收集作业或获取信息，回调函数的调用顺序如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;OnRequest
在发起请求前被调用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OnError
请求过程中如果发生错误被调用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OnResponse
收到回复后被调用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OnHTML
在OnResponse之后被调用，如果收到的内容是HTML&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OnScraped
在OnHTML之后被调用&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面我们看一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;

	&quot;github.com/gocolly/colly&quot;
)

func main() {
	// NewCollector(options ...func(*Collector)) *Collector
	// 声明初始化NewCollector对象时可以指定Agent，连接递归深度，URL过滤以及domain限制等
	c := colly.NewCollector(
		//colly.AllowedDomains(&quot;news.baidu.com&quot;),
		colly.UserAgent(&quot;Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.9.168 Version/11.50&quot;))

	// 发出请求时附的回调
	c.OnRequest(func(r *colly.Request) {
		// Request头部设定
		r.Headers.Set(&quot;Host&quot;, &quot;baidu.com&quot;)
		r.Headers.Set(&quot;Connection&quot;, &quot;keep-alive&quot;)
		r.Headers.Set(&quot;Accept&quot;, &quot;*/*&quot;)
		r.Headers.Set(&quot;Origin&quot;, &quot;&quot;)
		r.Headers.Set(&quot;Referer&quot;, &quot;http://www.baidu.com&quot;)
		r.Headers.Set(&quot;Accept-Encoding&quot;, &quot;gzip, deflate&quot;)
		r.Headers.Set(&quot;Accept-Language&quot;, &quot;zh-CN, zh;q=0.9&quot;)

		fmt.Println(&quot;Visiting&quot;, r.URL)
	})

	// 对响应的HTML元素处理
	c.OnHTML(&quot;title&quot;, func(e *colly.HTMLElement) {
		//e.Request.Visit(e.Attr(&quot;href&quot;))
		fmt.Println(&quot;title:&quot;, e.Text)
	})

	c.OnHTML(&quot;body&quot;, func(e *colly.HTMLElement) {
		// &amp;lt;div class=&quot;hotnews&quot; alog-group=&quot;focustop-hotnews&quot;&amp;gt; 下所有的a解析
		e.ForEach(&quot;.hotnews a&quot;, func(i int, el *colly.HTMLElement) {
			band := el.Attr(&quot;href&quot;)
			title := el.Text
			fmt.Printf(&quot;新闻 %d : %s - %s\n&quot;, i, title, band)
			// e.Request.Visit(band)
		})
	})

	// 发现并访问下一个连接
	//c.OnHTML(`.next a[href]`, func(e *colly.HTMLElement) {
	//	e.Request.Visit(e.Attr(&quot;href&quot;))
	//})

	// extract status code
	c.OnResponse(func(r *colly.Response) {
		fmt.Println(&quot;response received&quot;, r.StatusCode)
		// 设置context
		// fmt.Println(r.Ctx.Get(&quot;url&quot;))
	})

	// 对visit的线程数做限制，visit可以同时运行多个
	c.Limit(&amp;amp;colly.LimitRule{
		Parallelism: 2,
		//Delay:      5 * time.Second,
	})

	c.Visit(&quot;http://news.baidu.com&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码在开始处对Colly做了简单的初始化，增加UserAgent和域名限制，其他的设置可根据实际情况来设置，Url过滤，抓取深度等等都可以在此设置，也可以后运行时在具体设置。&lt;/p&gt;
&lt;p&gt;该例只是简单说明了Colly在爬虫抓取，调度管理方面的优势，对此如有兴趣可更深入了解。大家在深入学习Colly时，可自行选择更合适的URL。&lt;/p&gt;
&lt;p&gt;程序运行后，开始根据news.baidu.com抓取页面结果，通过OnHTML回调函数分析首页中的热点新闻标题及链接，并可不断地抓取更深层次的新链接进行访问，每个链接的访问结果我们可以通过OnHTML来进行分析，也可通过OnResponse来进行处理，例子中没有进一步展示深层链接的内容，有兴趣的朋友可以继续进一步研究。&lt;/p&gt;
&lt;p&gt;我们来看看OnHTML这个方法的定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (c *Collector) OnHTML(goquerySelector string, f HTMLCallback)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接在参数中标明了 goquerySelector ，上例中我们有简单尝试。这和我们下面要介绍的goquery HTML解析框架有一定联系，我们也可以使用goquery，通过goquery 来更轻松分析HTML代码。&lt;/p&gt;
&lt;h2&gt;41.2 goquery HTML解析&lt;/h2&gt;
&lt;p&gt;Colly框架可以快速发起请求，接收服务器响应。但如果我们需要分析返回的HTML代码，这时候仅仅使用Colly就有点吃力。而goquery库是一个使用Go语言写成的HTML解析库，功能更加强大。&lt;/p&gt;
&lt;p&gt;goquery将jQuery的语法和特性引入进来，所以可以更灵活地选择采集内容的数据项，就像jQuery那样的方式来操作DOM文档，使用起来非常的简便。&lt;/p&gt;
&lt;p&gt;goquery主要的结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Document struct {
	*Selection
	Url      *url.URL
	rootNode *html.Node
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Document 嵌入了Selection 类型，因此，Document 可以直接使用 Selection 类型的方法。我们可以通过下面四种方式来初始化得到*Document对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func NewDocumentFromNode(root *html.Node) *Document 

func NewDocument(url string) (*Document, error) 

func NewDocumentFromReader(r io.Reader) (*Document, error) 

func NewDocumentFromResponse(res *http.Response) (*Document, error)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Selection 是重要的一个结构体，解析中最重要，最核心的方法方法都由它提供。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Selection struct {
	Nodes    []*html.Node
	document *Document
	prevSel  *Selection
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面我们开始了解下怎么使用goquery：&lt;/p&gt;
&lt;p&gt;首先，要确定已经下载安装这个第三方包：&lt;/p&gt;
&lt;p&gt;go get github.com/PuerkitoBio/goquery&lt;/p&gt;
&lt;p&gt;接下来在代码中导入包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &quot;github.com/PuerkitoBio/goquery&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;goquery的主要用法是选择器，需要借鉴jQuery的特性，多加练习就能很快掌握。限于篇幅，这里只能简单介绍了goquery的大概情况。&lt;/p&gt;
&lt;p&gt;goquery可以直接发送url请求，获得响应后得到HTML代码。但goquery主要擅长于HTML代码分析，而Colly在爬虫抓取管理调度上有优势，所以下面以Colly作为爬虫框架，goquery作为HTML分析器，看看怎么抓取并分析页面内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;bytes&quot;
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;net/url&quot;
	&quot;time&quot;

	&quot;github.com/PuerkitoBio/goquery&quot;
	&quot;github.com/gocolly/colly&quot;
)

func main() {
	urlstr := &quot;https://news.baidu.com&quot;
	u, err := url.Parse(urlstr)
	if err != nil {
		log.Fatal(err)
	}
	c := colly.NewCollector()
	// 超时设定
	c.SetRequestTimeout(100 * time.Second)
	// 指定Agent信息
	c.UserAgent = &quot;Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36&quot;
	c.OnRequest(func(r *colly.Request) {
		// Request头部设定
		r.Headers.Set(&quot;Host&quot;, u.Host)
		r.Headers.Set(&quot;Connection&quot;, &quot;keep-alive&quot;)
		r.Headers.Set(&quot;Accept&quot;, &quot;*/*&quot;)
		r.Headers.Set(&quot;Origin&quot;, u.Host)
		r.Headers.Set(&quot;Referer&quot;, urlstr)
		r.Headers.Set(&quot;Accept-Encoding&quot;, &quot;gzip, deflate&quot;)
		r.Headers.Set(&quot;Accept-Language&quot;, &quot;zh-CN, zh;q=0.9&quot;)
	})

	c.OnHTML(&quot;title&quot;, func(e *colly.HTMLElement) {
		fmt.Println(&quot;title:&quot;, e.Text)
	})

	c.OnResponse(func(resp *colly.Response) {
		fmt.Println(&quot;response received&quot;, resp.StatusCode)

		// goquery直接读取resp.Body的内容
		htmlDoc, err := goquery.NewDocumentFromReader(bytes.NewReader(resp.Body))

		// 读取url再传给goquery，访问url读取内容，此处不建议使用
		// htmlDoc, err := goquery.NewDocument(resp.Request.URL.String())

		if err != nil {
			log.Fatal(err)
		}

		// 找到抓取项 &amp;lt;div class=&quot;hotnews&quot; alog-group=&quot;focustop-hotnews&quot;&amp;gt; 下所有的a解析
		htmlDoc.Find(&quot;.hotnews a&quot;).Each(func(i int, s *goquery.Selection) {
			band, _ := s.Attr(&quot;href&quot;)
			title := s.Text()
			fmt.Printf(&quot;热点新闻 %d: %s - %s\n&quot;, i, title, band)
			c.Visit(band)
		})

	})

	c.OnError(func(resp *colly.Response, errHttp error) {
		err = errHttp
	})

	err = c.Visit(urlstr)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，goquery先通过 goquery.NewDocumentFromReader生成文档对象htmlDoc。有了htmlDoc就可以使用选择器，而选择器的目的主要是定位：htmlDoc.Find(&quot;.hotnews a&quot;).Each(func(i int, s *goquery.Selection)，找到文档中的&amp;lt;div class=&quot;hotnews&quot; alog-group=&quot;focustop-hotnews&quot;&amp;gt;。&lt;/p&gt;
&lt;p&gt;有关选择器Find()方法的使用语法，是不是有些熟悉的感觉，没错就是jQuery的样子。&lt;/p&gt;
&lt;p&gt;在goquery中，常用大概有以下选择器：&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;HTML Element&lt;/td&gt;
&lt;td&gt;元素的选择器Find(&quot;a&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Element ID 选择器&lt;/td&gt;
&lt;td&gt;Find(element#id)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Class选择器&lt;/td&gt;
&lt;td&gt;Find(&quot;.class&quot;)&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;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;Find(“div[lang]“)&lt;/td&gt;
&lt;td&gt;筛选含有lang属性的div元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find(“div[lang=zh]“)&lt;/td&gt;
&lt;td&gt;筛选lang属性为zh的div元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find(“div[lang!=zh]“)&lt;/td&gt;
&lt;td&gt;筛选lang属性不等于zh的div元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find(“div[lang¦=zh]“)&lt;/td&gt;
&lt;td&gt;筛选lang属性为zh或者zh-开头的div元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find(“div[lang*=zh]“)&lt;/td&gt;
&lt;td&gt;筛选lang属性包含zh这个字符串的div元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find(“div[lang~=zh]“)&lt;/td&gt;
&lt;td&gt;筛选lang属性包含zh这个单词的div元素，单词以空格分开的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find(“div[lang$=zh]“)&lt;/td&gt;
&lt;td&gt;筛选lang属性以zh结尾的div元素，区分大小写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find(“div[lang^=zh]“)&lt;/td&gt;
&lt;td&gt;筛选lang属性以zh开头的div元素，区分大小写&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;parent&amp;gt;child选择器
如果我们想筛选出某个元素下符合条件的子元素，我们就可以使用子元素筛选器，它的语法为Find(&quot;parent&amp;gt;child&quot;),表示筛选parent这个父元素下，符合child这个条件的最直接（一级）的子元素。&lt;/p&gt;
&lt;p&gt;prev+next相邻选择器
假设我们要筛选的元素没有规律，但是该元素的上一个元素有规律，我们就可以使用这种下一个相邻选择器来进行选择。&lt;/p&gt;
&lt;p&gt;prev~next选择器
有相邻就有兄弟，兄弟选择器就不一定要求相邻了，只要他们共有一个父元素就可以。&lt;/p&gt;
&lt;p&gt;Colly + goquery 是抓取网络内容的利器，使用上极其方便。如今动态渲染的页面越来越多，爬虫们或多或少都需要用到headless browser来渲染待爬取的页面，这里推荐chromedp，开源网址：https://github.com/chromedp/chromedp&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_40_kvdb.md&quot;&gt;第四十章 LevelDB与BoltDB&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_42_gin.md&quot;&gt;第四十二章 WEB框架(Gin)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第四十章 LevelDB与BoltDB</title><link>https://blog.wemang.com/posts/go/study/42_40_kvdb/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_40_kvdb/</guid><pubDate>Wed, 21 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;LevelDB 和 BoltDB 都是k/v非关系型数据库。&lt;/p&gt;
&lt;p&gt;LevelDB没有事务，LevelDB实现了一个日志结构化的merge tree。它将有序的key/value存储在不同文件的之中，通过db, _ := leveldb.OpenFile(&quot;db&quot;, nil)，在db目录下有很多数据文件，并通过“层级”把它们分开，并且周期性地将小的文件merge为更大的文件。这让其在随机写的时候会很快，但是读的时候却很慢。&lt;/p&gt;
&lt;p&gt;这也让LevelDB的性能不可预知：但数据量很小的时候，它可能性能很好，但是当随着数据量的增加，性能只会越来越糟糕。而且做merge的线程也会在服务器上出现问题。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;LSM树而且通过批量存储技术规避磁盘随机写入问题。 LSM树的设计思想非常朴素，它的原理是把一颗大树拆分成N棵小树， 它首先写入到内存中（内存没有寻道速度的问题，随机写的性能得到大幅提升），在内存中构建一颗有序小树，随着小树越来越大，内存的小树会flush到磁盘上。磁盘中的树定期可以做merge操作，合并成一棵大树，以优化读性能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;BoltDB会在数据文件上获得一个文件锁，所以多个进程不能同时打开同一个数据库。BoltDB使用一个单独的内存映射的文件(.db)，实现一个写入时拷贝的B+树，这能让读取更快。而且，BoltDB的载入时间很快，特别是在从crash恢复的时候，因为它不需要去通过读log去找到上次成功的事务，它仅仅从两个B+树的根节点读取ID。&lt;/p&gt;
&lt;p&gt;BoltDB支持完全可序列化的ACID事务，让应用程序可以更简单的处理复杂操作。&lt;/p&gt;
&lt;p&gt;BoltDB设计源于LMDB，具有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接使用API存取数据，没有查询语句；&lt;/li&gt;
&lt;li&gt;支持完全可序列化的ACID事务，这个特性比LevelDB强；&lt;/li&gt;
&lt;li&gt;数据保存在内存映射的文件里。没有wal、线程压缩和垃圾回收；&lt;/li&gt;
&lt;li&gt;通过COW技术，可实现无锁的读写并发，但是无法实现无锁的写写并发，这就注定了读性能超高，但写性能一般，适合与读多写少的场景。&lt;/li&gt;
&lt;li&gt;最后，BoltDB使用Golang开发，而且被应用于influxDB项目作为底层存储。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;LMDB的全称是Lightning Memory-Mapped Database(快如闪电的内存映射数据库)，它的文件结构简单，包含一个数据文件和一个锁文件.
LMDB文件可以同时由多个进程打开，具有极高的数据存取速度，访问简单，不需要运行单独的数据库管理进程，只要在访问数据的代码里引用LMDB库，访问时给文件路径即可。&lt;/p&gt;
&lt;p&gt;让系统访问大量小文件的开销很大，而LMDB使用内存映射的方式访问文件，使得文件内寻址的开销非常小，使用指针运算就能实现。数据库单文件还能减少数据集复制/传输过程的开销。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;40.1 LevelDB&lt;/h2&gt;
&lt;p&gt;Go语言LevelDB的实现我们使用 github.com/syndtr/goleveldb/leveldb 包，通过go get命令下载该包后在程序中导入。&lt;/p&gt;
&lt;p&gt;goleveldb主要有Get()，Put()等方法，可进行key/value的读取和写入，可进行事务批量Put()插入key，Delete()删除某个key。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;strconv&quot;

	&quot;crypto/md5&quot;

	&quot;github.com/syndtr/goleveldb/leveldb&quot;
	&quot;github.com/syndtr/goleveldb/leveldb/util&quot;
)

var md = md5.New()

// 测试专用
func Read(db *leveldb.DB, num int) {
	var kStr string
	var haskKey string
	kStr = strconv.Itoa(num)
	md.Write([]byte(kStr))
	haskKey = fmt.Sprintf(&quot;%x&quot;, md.Sum(nil))
	md.Reset()

	db.Get([]byte(haskKey), nil)
}

// 测试专用
func Write(db *leveldb.DB, num int) {
	var kStr string
	var haskKey string
	kStr = strconv.Itoa(num)
	md.Write([]byte(kStr))
	haskKey = fmt.Sprintf(&quot;%x&quot;, md.Sum(nil))
	md.Reset()

	db.Put([]byte(haskKey), []byte(kStr), nil)
}

func main() {
	// 打开数据库文件 /path/to/db ,第一个参数为存放数据的目录，不是具体文件
	// o := &amp;amp;opt.Options{	Filter: filter.NewBloomFilter(10),}
	// OpenFile第2个参数这里指定为nil，在数据集大时可设置比如布隆过滤器。
	// *opt.Options 为nil默认为false ，true为只读模式ReadOnly
	db, _ := leveldb.OpenFile(&quot;levdb&quot;, nil)

	defer db.Close()

	// 读数据库:Get(key,nil)，写数据库:Put(key,value,nil)
	// Put第三个参数为nil，默认就好，默认时写的时候如果机器崩了数据会丢失。
	// key和value都是字节slice
	_ = db.Put([]byte(&quot;key1&quot;), []byte(&quot;好好检查&quot;), nil)
	_ = db.Put([]byte(&quot;key2&quot;), []byte(&quot;天天向上&quot;), nil)
	_ = db.Put([]byte(&quot;key:3&quot;), []byte(&quot;就会一个本事&quot;), nil)
	_ = db.Put([]byte(&quot;uname&quot;), []byte(&quot;Jim&quot;), nil)
	_ = db.Put([]byte(&quot;time&quot;), []byte(&quot;1450932202&quot;), nil)

	// 读数据库:Get(key,nil)，返回字节slice
	data, _ := db.Get([]byte(&quot;key1&quot;), nil)
	fmt.Println(&quot;key1=&amp;gt;&quot;, string(data))

	// 删除某个key(key,nil)，key不存在时并不返回错误
	_ = db.Delete([]byte(&quot;key&quot;), nil)

	//迭代数据库内容:
	iter := db.NewIterator(nil, nil)
	fmt.Println(&quot;迭代所有key/value&quot;)
	for iter.Next() {
		key := iter.Key()
		value := iter.Value()
		fmt.Println(string(key), &quot;=&amp;gt;&quot;, string(value))

	}
	iter.Release()
	iter.Error()

	//Seek()定位到比给定key值(字节值)要大的第一个key，可next迭代所有筛选出的key/value:
	iter = db.NewIterator(nil, nil)
	fmt.Println(&quot;\nSeek()按值筛选查找key&quot;)
	for ok := iter.Seek([]byte(&quot;t&quot;)); ok; ok = iter.Next() {
		// Use key/value.
		fmt.Println(&quot;Seek-then-Iterate:&quot;)
		fmt.Println(string(iter.Key()), &quot;=&amp;gt;&quot;, string(iter.Value()))
	}
	iter.Release()

	//迭代内容子集:start表示key中包含有的字符串， Limit表示key不能包含有字符串
	fmt.Println(&quot;\n 按照指定（排除）条件筛选key&quot;)
	iter = db.NewIterator(&amp;amp;util.Range{Start: []byte(&quot;key&quot;), Limit: []byte(&quot;no&quot;)}, nil)
	for iter.Next() {
		// Use key/value.
		fmt.Println(&quot;Iterate over subset of database content:&quot;)
		fmt.Println(string(iter.Key()), &quot;=&amp;gt;&quot;, string(iter.Value()))
	}
	iter.Release()

	//迭代子集内容，key的前缀是指定字符串:
	fmt.Println(&quot;\n 查找指定前缀key&quot;)
	iter = db.NewIterator(util.BytesPrefix([]byte(&quot;key&quot;)), nil)
	for iter.Next() {
		// Use key/value.
		fmt.Println(&quot;Iterate over subset of database content with a particular prefix:&quot;)
		fmt.Println(string(iter.Key()), &quot;=&amp;gt;&quot;, string(iter.Value()))
	}
	iter.Release()

	_ = iter.Error()

	//批量写:
	batch := new(leveldb.Batch)
	var kStr string
	var batchkey string
	for i := 0; i &amp;lt; 10; i++ {
		kStr = strconv.Itoa(i)
		md.Write([]byte(kStr))
		batchkey = fmt.Sprintf(&quot;%x&quot;, md.Sum(nil))
		batch.Put([]byte(batchkey), []byte(kStr))
	}
	md.Reset()
	batch.Delete([]byte(&quot;lazy&quot;))
	_ = db.Write(batch, nil)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Leveldb比较突出的问题是在读操作上，在大量key的情况下可能成为性能的瓶颈，我们可以根据场景来选择使用。下面是我们进行的几种数量级别的基准测试数据：&lt;/p&gt;
&lt;p&gt;BenchmarkWrite-4   	  100000	     14541 ns/op
BenchmarkRead-4    	  100000	     13094 ns/op&lt;/p&gt;
&lt;p&gt;BenchmarkWrite-4   	  500000	     12724 ns/op
BenchmarkRead-4    	  500000	     17002 ns/op&lt;/p&gt;
&lt;p&gt;BenchmarkWrite-4   	 1000000	     13355 ns/op
BenchmarkRead-4    	 1000000	     20610 ns/op&lt;/p&gt;
&lt;p&gt;BenchmarkWrite-4   	 3000000	     15644 ns/op
BenchmarkRead-4    	 3000000	     22742 ns/op&lt;/p&gt;
&lt;p&gt;我们可以看到随着key的数量的增加，读的性能明显地下降，而写的性能则不受影响。&lt;/p&gt;
&lt;h2&gt;40.2 BoltDB&lt;/h2&gt;
&lt;p&gt;Go语言BoltDB的实现我们使用 github.com/boltdb/bolt 包，通过go get命令下载该包后在程序中导入。&lt;/p&gt;
&lt;p&gt;BoltDB中存储比较重要的概念是bucket，存取操作之前都需要指定bucket，如果读数据时指定bucket不存在，则会panic。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;bytes&quot;
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;time&quot;

	&quot;github.com/boltdb/bolt&quot;
)

func main() {
	Boltdb()
}
func Boltdb() error {
	// Bolt 会在数据文件上获得一个文件锁，所以多个进程不能同时打开同一个数据库。
	// 打开一个已经打开的 Bolt 数据库将导致它挂起，直到另一个进程关闭它。
	// 为防止无限期等待，您可以将超时选项传递给Open()函数：
	db, err := bolt.Open(&quot;my.db&quot;, 0600, &amp;amp;bolt.Options{Timeout: 10 * time.Second})
	defer db.Close()
	if err != nil {
		log.Fatal(err)
	}

	//	两种处理方式：读-写和只读操作，读-写方式开始于db.Update方法：
	//	Bolt 一次只允许一个读写事务，但是一次允许多个只读事务。
	// 每个事务处理都有一个始终如一的数据视图
	err = db.Update(func(tx *bolt.Tx) error {
		// 这里还有另外一层：k-v存储在bucket中，
		// 可以将bucket当做一个key的集合或者是数据库中的表。
		//（顺便提一句，buckets中可以包含其他的buckets，这将会相当有用）
		// Buckets 是键值对在数据库中的集合.所有在bucket中的key必须唯一，
		// 使用DB.CreateBucket() 函数建立buket
		// Tx.DeleteBucket() 删除bucket
		// b := tx.Bucket([]byte(&quot;MyBucket&quot;))
		b, err := tx.CreateBucketIfNotExists([]byte(&quot;MyBucket&quot;))

		//要将 key/value 对保存到 bucket，请使用 Bucket.Put() 函数：
		//这将在 MyBucket 的 bucket 中将 &quot;answer&quot; key的值设置为&quot;42&quot;。
		err = b.Put([]byte(&quot;answer&quot;), []byte(&quot;42&quot;))
		err = b.Put([]byte(&quot;why&quot;), []byte(&quot;101010&quot;))
		return err
	})

	// 可以看到，传入db.update函数一个参数，在函数内部你可以get/set数据和处理error。
	// 如返回为nil，事务就会从数据库得到一个commit，但如果返回一个实际的错误，则会做回滚，
	// 你在函数中做的事情都不会commit。这很自然，因为你不需要人为地去关心事务的回滚，
	// 只需要返回一个错误，其他的由Bolt去帮你完成。
	// 只读事务 只读事务和读写事务不应该相互依赖，一般不应该在同一个例程中同时打开。
	// 这可能会导致死锁，因为读写事务需要定期重新映射数据文件，
	// 但只有在只读事务处于打开状态时才能这样做。

	// 批量读写事务.每一次新的事物都需要等待上一次事物的结束，
	// 可以通过DB.Batch()批处理来完
	err = db.Batch(func(tx *bolt.Tx) error {
		return nil
	})

	//只读事务在db.View函数之中：在函数中可以读取，但是不能做修改。
	db.View(func(tx *bolt.Tx) error {
		//要检索这个value，我们可以使用 Bucket.Get() 函数：
		//由于Get是有安全保障的，所有不会返回错误，不存在的key返回nil
		b := tx.Bucket([]byte(&quot;MyBucket&quot;))
		//tx.Bucket([]byte(&quot;MyBucket&quot;)).Cursor() 可这样写
		v := b.Get([]byte(&quot;answer&quot;))
		id, _ := b.NextSequence()
		fmt.Printf(&quot;The answer is: %s %d \n&quot;, v, id)

		//游标遍历key
		c := b.Cursor()
		fmt.Println(&quot;\n游标遍历key&quot;)
		for k, v := c.First(); k != nil; k, v = c.Next() {
			fmt.Printf(&quot;key=%s, value=%s\n&quot;, k, v)
		}

		//游标上有以下函数：
		//First()  移动到第一个健.
		//Last()   移动到最后一个健.
		//Seek()   移动到特定的一个健.
		//Next()   移动到下一个健.
		//Prev()   移动到上一个健.

		//Prefix 前缀扫描
		fmt.Println(&quot;\nPrefix 前缀扫描&quot;)
		prefix := []byte(&quot;a&quot;)
		for k, v := c.Seek(prefix); k != nil &amp;amp;&amp;amp; bytes.HasPrefix(k, prefix); k, v = c.Next() {
			fmt.Printf(&quot;key=%s, value=%s\n&quot;, k, v)
		}
		return nil
	})

	//如果你知道所在桶中拥有键，你也可以使用ForEach()来迭代：
	db.View(func(tx *bolt.Tx) error {
		fmt.Println(&quot;\nForEach()来迭代&quot;)
		b := tx.Bucket([]byte(&quot;MyBucket&quot;))
		b.ForEach(func(k, v []byte) error {
			fmt.Printf(&quot;key=%s, value=%s\n&quot;, k, v)
			return nil
		})
		return nil
	})

	//事务处理
	// 开始事务
	tx, err := db.Begin(true)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	// 使用事务...
	_, err = tx.CreateBucket([]byte(&quot;MyBucket&quot;))
	if err != nil {
		return err
	}

	// 事务提交
	if err = tx.Commit(); err != nil {
		return err
	}
	return err

	//还可以在一个键中存储一个桶，以创建嵌套的桶：
	//func (*Bucket) CreateBucket(key []byte) (*Bucket, error)
	//func (*Bucket) CreateBucketIfNotExists(key []byte) (*Bucket, error)
	//func (*Bucket) DeleteBucket(key []byte) error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BoltDB的性能测试这里就不再做阐述，和LevelDB正好相反，它在写性能上存在瓶颈，而读性能上非常有优势，这两者我们需要根据场景来选择使用。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_39_mysql.md&quot;&gt;第三十九章 Mysql数据库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_41_crawler.md&quot;&gt;第四十一章 网络爬虫&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第三十九章 MySql数据库</title><link>https://blog.wemang.com/posts/go/study/42_39_mysql/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_39_mysql/</guid><pubDate>Tue, 20 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;39.1 database/sql包&lt;/h2&gt;
&lt;p&gt;Go 提供了database/sql包用于对关系型数据库的访问，作为操作数据库的入口对象sql.DB，主要为我们提供了两个重要的功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sql.DB 通过数据库驱动为我们提供管理底层数据库连接的打开和关闭操作.&lt;/li&gt;
&lt;li&gt;sql.DB 为我们管理数据库连接池&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要注意的是，sql.DB表示操作数据库的抽象访问接口, 而非一个数据库连接对象;它可以根据driver打开关闭数据库连接，管理连接池。正在使用的连接被标记为繁忙，用完后回到连接池等待下次使用。所以，如果你没有把连接释放回连接池，会导致过多连接使系统资源耗尽。&lt;/p&gt;
&lt;p&gt;具体到某一类型的关系型数据库，需要导入对应的数据库驱动。下面以MySQL8.0为例，来讲讲怎么在Go语言中调用。&lt;/p&gt;
&lt;p&gt;首先，需要下载第三方包：&lt;/p&gt;
&lt;p&gt;go get github.com/go-sql-driver/mysql&lt;/p&gt;
&lt;p&gt;在代码中导入mysql数据库驱动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
   &quot;database/sql&quot;
   _ &quot;github.com/go-sql-driver/mysql&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通常来说，不应该直接使用驱动所提供的方法，而是应该使用 sql.DB，因此在导入 mysql 驱动时，这里使用了匿名导入的方式(在包路径前添加 _)，当导入了一个数据库驱动后，此驱动会自行初始化并注册自己到Go的database/sql上下文中，因此我们就可以通过 database/sql 包提供的方法访问数据库了。&lt;/p&gt;
&lt;h2&gt;39.2 MySQL数据库操作&lt;/h2&gt;
&lt;p&gt;我们先建立表结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE t_article_cate (
`cid` int(10) NOT NULL AUTO_INCREMENT, 
  `cname` varchar(60) NOT NULL, 
  `ename` varchar(100), 
  `cateimg` varchar(255), 
  `addtime` int(10) unsigned NOT NULL DEFAULT &apos;0&apos;, 
  `publishtime` int(10) unsigned NOT NULL DEFAULT &apos;0&apos;, 
  `scope` int(10) unsigned NOT NULL DEFAULT &apos;10000&apos;, 
  `status` tinyint(1) unsigned NOT NULL DEFAULT &apos;0&apos;, 
  PRIMARY KEY (`cid`), 
  UNIQUE  KEY catename (`cname`)
) ENGINE=InnoDB AUTO_INCREMENT=99 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于预编译语句(PreparedStatement)提供了诸多好处，可以实现自定义参数的查询，通常来说，比手动拼接字符串 SQL 语句高效，可以防止SQL注入攻击。&lt;/p&gt;
&lt;p&gt;下面代码使用预编译的方式，来进行增删改查的操作，并通过事务来批量提交一批数据。&lt;/p&gt;
&lt;p&gt;在Go语言中对数据类型要求很严格，一般查询数据时先定义数据类型，但是查询数据库中的数据存在三种可能:
存在值，存在零值，未赋值NULL 三种状态，因此可以将待查询的数据类型定义为sql.Nullxxx类型，可以通过判断Valid值来判断查询到的值是否为赋值状态还是未赋值NULL状态。如: sql.NullInt64 sql.NullString&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;database/sql&quot;
	&quot;fmt&quot;
	&quot;strings&quot;
	&quot;time&quot;

	_ &quot;github.com/go-sql-driver/mysql&quot;
)

type DbWorker struct {
	Dsn string
	Db  *sql.DB
}

type Cate struct {
	cid     int
	cname   string
	addtime int
	scope   int
}



func main() {
	dbw := DbWorker{Dsn: &quot;root:123456@tcp(localhost:3306)/mydb?charset=utf8mb4&quot;}
	// 支持下面几种DSN写法，具体看MySQL服务端配置，常见为第2种
	// user@unix(/path/to/socket)/dbname?charset=utf8
	// user:password@tcp(localhost:5555)/dbname?charset=utf8
	// user:password@/dbname
	// user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname

	dbtemp,  err := sql.Open(&quot;mysql&quot;,  dbw.Dsn)
	dbw.Db = dbtemp

	if err != nil {
		panic(err)
		return
	}
	defer dbw.Db.Close()

	// 插入数据测试
	dbw.insertData()

	// 删除数据测试
	dbw.deleteData()

	// 修改数据测试
	dbw.editData()

	// 查询数据测试
	dbw.queryData()

	// 事务操作测试
	dbw.transaction()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次db.Query操作后，都建议调用rows.Close()。 因为 db.Query() 会从数据库连接池中获取一个连接，这个底层连接在结果集(rows)未关闭前会被标记为处于繁忙状态。当遍历读到最后一条记录时，会发生一个内部EOF错误，自动调用rows.Close(), 但如果提前退出循环，rows不会关闭，连接不会回到连接池中，连接也不会关闭，则此连接会一直被占用。 因此通常我们使用 defer rows.Close() 来确保数据库连接可以正确放回到连接池中。&lt;/p&gt;
&lt;p&gt;插入数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 插入数据，sql预编译
func (dbw *DbWorker) insertData() {
	stmt,  _ := dbw.Db.Prepare(`INSERT INTO t_article_cate (cname, addtime, scope) VALUES (?, ?, ?)`)
	defer stmt.Close()
	
	ret,  err := stmt.Exec(&quot;栏目1&quot;,  time.Now().UNIX(),  10)

	// 通过返回的ret可以进一步查询本次插入数据影响的行数
	// RowsAffected和最后插入的Id(如果数据库支持查询最后插入Id)
	if err != nil {
		fmt.Printf(&quot;insert data error: %v\n&quot;,  err)
		return
	}
	if LastInsertId,  err := ret.LastInsertId(); nil == err {
		fmt.Println(&quot;LastInsertId:&quot;,  LastInsertId)
	}
	if RowsAffected,  err := ret.RowsAffected(); nil == err {
		fmt.Println(&quot;RowsAffected:&quot;,  RowsAffected)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 删除数据，预编译
func (dbw *DbWorker) deleteData() {
	stmt,  err := dbw.Db.Prepare(`DELETE FROM t_article_cate WHERE cid=?`)
	ret,  err := stmt.Exec(122)
	// 通过返回的ret可以进一步查询本次插入数据影响的行数RowsAffected和
	// 最后插入的Id(如果数据库支持查询最后插入Id).
	if err != nil {
		fmt.Printf(&quot;insert data error: %v\n&quot;,  err)
		return
	}
	if RowsAffected,  err := ret.RowsAffected(); nil == err {
		fmt.Println(&quot;RowsAffected:&quot;,  RowsAffected)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 修改数据，预编译
func (dbw *DbWorker) editData() {
	stmt,  err := dbw.Db.Prepare(`UPDATE t_article_cate SET scope=? WHERE cid=?`)
	ret,  err := stmt.Exec(111,  123)
	// 通过返回的ret可以进一步查询本次插入数据影响的行数RowsAffected和
// 最后插入的Id(如果数据库支持查询最后插入Id).
	if err != nil {
		fmt.Printf(&quot;insert data error: %v\n&quot;,  err)
		return
	}
	if RowsAffected,  err := ret.RowsAffected(); nil == err {
		fmt.Println(&quot;RowsAffected:&quot;,  RowsAffected)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查询数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 查询数据，预编译
func (dbw *DbWorker) queryData() {
	// 如果方法包含Query，那么这个方法是用于查询并返回rows的。其他用Exec()
// 另外一种写法
	// rows, err := db.Query(&quot;select id, name from users where id = ?&quot;, 1) 
	stmt,  _ := dbw.Db.Prepare(`SELECT cid, cname, addtime, scope From t_article_cate where status=?`)
	//err = db.QueryRow(&quot;select name from users where id = ?&quot;, 1).Scan(&amp;amp;name) // 单行查询，直接处理
	defer stmt.Close()

	rows,  err := stmt.Query(0)
	defer rows.Close()
	if err != nil {
		fmt.Printf(&quot;insert data error: %v\n&quot;,  err)
		return
	}

	// 构造scanArgs、values两个slice，
// scanArgs的每个值指向values相应值的地址
	columns,  _ := rows.Columns()
	fmt.Println(columns)
	rowMaps := make([]map[string]string,  9)
	values := make([]sql.RawBytes,  len(columns))
	scans := make([]interface{},  len(columns))
	for i := range values {
		scans[i] = &amp;amp;values[i]
		scans[i] = &amp;amp;values[i]
	}
	i := 0
	for rows.Next() {
		//将行数据保存到record字典
		err = rows.Scan(scans...)

		each := make(map[string]string,  4)
    // 由于是map引用，放在上层for时，rowMaps最终返回值是最后一条。
		for i,  col := range values {
			each[columns[i]] = string(col)
		}

// 切片追加数据，索引位置有意思。不这样写就不是希望的样子。
		rowMaps = append(rowMaps[:i],  each) 
		fmt.Println(each)
		i++
	}
	fmt.Println(rowMaps)

	for i,  col := range rowMaps {
		fmt.Println(i,  col)
	}

	err = rows.Err()
	if err != nil {
		fmt.Printf(err.Error())
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务处理：
db.Begin()开始事务，Commit() 或 Rollback()关闭事务。Tx从连接池中取出一个连接，在关闭之前都使用这个连接。Tx不能和DB层的BEGIN，COMMIT混合使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (dbw *DbWorker) transaction() {
	tx,  err := dbw.Db.Begin()
	if err != nil {

		fmt.Printf(&quot;insert data error: %v\n&quot;,  err)
		return
	}
	defer tx.Rollback()
	stmt,  err := tx.Prepare(`INSERT INTO t_article_cate (cname, addtime, scope) VALUES (?, ?, ?)`)
	if err != nil {

		fmt.Printf(&quot;insert data error: %v\n&quot;,  err)
		return
	}

	for i := 100; i &amp;lt; 110; i++ {
		cname := strings.Join([]string{&quot;栏目-&quot;,  string(i)},  &quot;-&quot;)
		_,  err = stmt.Exec(cname,  time.Now().UNIX(),  i+20)
		if err != nil {
			fmt.Printf(&quot;insert data error: %v\n&quot;,  err)
			return
		}
	}
	err = tx.Commit()
	if err != nil {
		fmt.Printf(&quot;insert data error: %v\n&quot;,  err)
		return
	}
	stmt.Close()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_38_json.md&quot;&gt;第三十八章 数据序列化&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_40_kvdb.md&quot;&gt;第四十章 LevelDB与BoltDB&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第三十八章 数据序列化</title><link>https://blog.wemang.com/posts/go/study/42_38_json/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_38_json/</guid><pubDate>Mon, 19 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;38.1 序列化与反序列化&lt;/h2&gt;
&lt;p&gt;我们的数据对象要在网络中传输或保存到文件，就需要对其编码和解码动作，目前存在很多编码格式：JSON，XML，Gob，Google Protocol Buffer等，Go 语言当然也支持所有这些编码格式。&lt;/p&gt;
&lt;p&gt;序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间，对象将其当前状态写入到临时或持久性存储区。通过从存储区中读取对象的状态，重新创建该对象，则为反序列化。&lt;/p&gt;
&lt;p&gt;简单地说把某种数据结构转为指定数据格式为“序列化”或“编码”（传输之前）；而把“指定数据格式”转为某种数据结构则为“反序列化”或“解码”（传输之后）。&lt;/p&gt;
&lt;p&gt;在Go语言中，encoding/json标准包处理JSON数据的序列化与反序列化问题。&lt;/p&gt;
&lt;p&gt;JSON数据序列化函数主要有：&lt;/p&gt;
&lt;p&gt;json.Marshal()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Marshal(v interface{}) ([]byte, error) {
	e := newEncodeState()

	err := e.marshal(v, encOpts{escapeHTML: true})
	if err != nil {
		return nil, err
	}
	buf := append([]byte(nil), e.Bytes()...)

	e.Reset()
	encodeStatePool.Put(e)

	return buf, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的Marshal()函数我们可以看到，数据结构序列化后返回的是字节数组，而字节数组很容易通过网络传输或写入文件存储。而且在Go中，Marshal()默认是设置escapeHTML = true的，会自动把 &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, 以及 &lt;code&gt;&amp;amp;&lt;/code&gt; 等转化为&quot;\u003c&quot; ， &quot;\u003e&quot;以及 &quot;\u0026&quot;。&lt;/p&gt;
&lt;p&gt;JSON数据反序列化函数主要有：&lt;/p&gt;
&lt;p&gt;UnMarshal()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Unmarshal(data []byte, v interface{}) error // 把 JSON 解码为数据结构
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的UnMarshal()函数我们可以看到，反序列化是读取字节数组，进而解析为对应的数据结构。&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;p&gt;不是所有的数据都可以编码为 JSON 格式,只有验证通过的数据结构才能被编码：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;json 对象只支持字符串类型的 key；要编码一个 Go map 类型，map 必须是 map[string]T（T是 json 包中支持的任何类型）&lt;/li&gt;
&lt;li&gt;channel，复杂类型和函数类型不能被编码&lt;/li&gt;
&lt;li&gt;不支持循环数据结构；它将引起序列化进入一个无限循环&lt;/li&gt;
&lt;li&gt;指针可以被编码，实际上是对指针指向的值进行编码（或者指针是 nil）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而在Go中，JSON 与 Go 类型对应如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bool    对应 JSON 的 booleans&lt;/li&gt;
&lt;li&gt;float64 对应 JSON 的 numbers&lt;/li&gt;
&lt;li&gt;string  对应 JSON 的 strings&lt;/li&gt;
&lt;li&gt;nil     对应 JSON 的 null&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在解析 JSON 格式数据时，若以 interface{} 接收数据，需要按照以上规则进行解析。&lt;/p&gt;
&lt;h2&gt;38.2 JSON数据格式&lt;/h2&gt;
&lt;p&gt;在Go语言中，利用encoding/json标准包将数据序列化为JSON数据格式这个过程简单直接，直接使用json.Marshal(v)来处理任意类型，序列化成功后得到一个字节数组。&lt;/p&gt;
&lt;p&gt;反过来我们将一个JSON数据来反序列化或解码，则就不那么容易了，下面我们一一来说明。&lt;/p&gt;
&lt;p&gt;（一）将JSON数据反序列化到结构体：&lt;/p&gt;
&lt;p&gt;这种需求是最常见的，在我们知道 JSON 的数据结构前提情况下，我们完全可以定义一个或几个适当的结构体并对 JSON 数据反序列化。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;encoding/json&quot;
	&quot;fmt&quot;
)

type Human struct {
	name   string `json:&quot;name&quot;` // 姓名
	Gender  string `json:&quot;s&quot;`    // 性别，性别的tag表明在json中为s字段
	Age    int    `json:&quot;Age&quot;`  // 年龄
	Lesson
}

type Lesson struct {
	Lessons []string `json:&quot;lessons&quot;`
}

func main() {
	jsonStr := `{&quot;Age&quot;: 18,&quot;name&quot;: &quot;Jim&quot; ,&quot;s&quot;: &quot;男&quot;,
	&quot;lessons&quot;:[&quot;English&quot;,&quot;History&quot;],&quot;Room&quot;:201,&quot;n&quot;:null,&quot;b&quot;:false}`

	var hu Human
	if err := json.Unmarshal([]byte(jsonStr), &amp;amp;hu); err == nil {
		fmt.Println(&quot;\n结构体Human&quot;)
		fmt.Println(hu)
	}

	var le Lesson
	if err := json.Unmarshal([]byte(jsonStr), &amp;amp;le); err == nil {
		fmt.Println(&quot;\n结构体Lesson&quot;)
		fmt.Println(le)
	}

	jsonStr = `[&quot;English&quot;,&quot;History&quot;]`

	var str []string
	if err := json.Unmarshal([]byte(jsonStr), &amp;amp;str); err == nil {
		fmt.Println(&quot;\n字符串数组&quot;)
		fmt.Println(str)
	} else {
		fmt.Println(err)
	}
}

程序输出：
结构体Human
{ 男 18 {[English History]}}

结构体Lesson
{[English History]}

字符串数组
[English History 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们定义了2个结构体Human和Lesson，结构体Human的Gender字段tag标签为：&lt;code&gt;json:&quot;s&quot;&lt;/code&gt;，表明这个字段在JSON中的名字对应为s。而且结构体Human中嵌入了Lesson结构体。&lt;/p&gt;
&lt;p&gt;jsonStr 我们可以认作为一个JSON数据，通过json.Unmarshal，我们可以把JSON中的数据反序列化到了对应结构体，由于结构体Human的name字段不能导出，所以并不能实际得到JSON中的值，这是我们在定义结构体时需要注意的，字段首字母大写。&lt;/p&gt;
&lt;p&gt;对JSON中的Age，在结构体Human对应Age int，不能是string。另外，如果是JSON数组，可以把数据反序列化给一个字符串数组。&lt;/p&gt;
&lt;p&gt;总之，知道JSON的数据结构很关键，有了这个前提做反序列化就容易多了。而且结构体的字段并不需要和JSON中所有数据都一一对应，定义的结构体字段可以是JSON中的一部分。&lt;/p&gt;
&lt;p&gt;（二）反序列化任意JSON数据：&lt;/p&gt;
&lt;p&gt;encoding/json 包使用 map[string]interface{} 和 []interface{} 储存任意的 JSON 对象和数组；其可以被反序列化为任何的 JSON blob 存储到接口值中。&lt;/p&gt;
&lt;p&gt;直接使用 Unmarshal 把这个数据反序列化，并保存在map[string]interface{} 中，要访问这个数据，我们可以使用类型断言：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;encoding/json&quot;
	&quot;fmt&quot;
)

func main() {
	jsonStr := `{&quot;Age&quot;: 18,&quot;name&quot;: &quot;Jim&quot; ,&quot;s&quot;: &quot;男&quot;,&quot;Lessons&quot;:[&quot;English&quot;,&quot;History&quot;],&quot;Room&quot;:201,&quot;n&quot;:null,&quot;b&quot;:false}`

	var data map[string]interface{}
	if err := json.Unmarshal([]byte(jsonStr), &amp;amp;data); err == nil {
		fmt.Println(&quot;map结构&quot;)
		fmt.Println(data)
	}

	for k, v := range data {
		switch vv := v.(type) {
		case string:
			fmt.Println(k, &quot;是string&quot;, vv)
		case bool:
			fmt.Println(k, &quot;是bool&quot;, vv)
		case float64:
			fmt.Println(k, &quot;是float64&quot;, vv)
		case nil:
			fmt.Println(k, &quot;是nil&quot;, vv)
		case []interface{}:
			fmt.Println(k, &quot;是array:&quot;)
			for i, u := range vv {
				fmt.Println(i, u)
			}
		default:
			fmt.Println(k, &quot;未知数据类型&quot;)
		}
	}
}

程序输出：
map结构
map[n:&amp;lt;nil&amp;gt; b:false Age:18 name:Jim s:男 Lessons:[English History] Room:201]
name 是string Jim
s 是string 男
Lessons 是array:
0 English
1 History
Room 是float64 201
n 是nil &amp;lt;nil&amp;gt;
b 是bool false
Age 是float64 18
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这种方式，即使是未知 JSON 数据结构，我们也可以反序列化，同时可以确保类型安全。在switch-type中，我们可以根据表16-3 JSON与Go数据类型对照表来做选择。比如Age是float64而不是int类型，另外JSON的booleans、null类型在JSON也常常出现，在这里都做了case。&lt;/p&gt;
&lt;p&gt;（三）JSON数据编码和解码：&lt;/p&gt;
&lt;p&gt;json 包提供 Decoder 和 Encoder 类型来支持常用 JSON 数据流读写。NewDecoder 和 NewEncoder 函数分别封装了 io.Reader 和 io.Writer 接口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要想把 JSON 直接写入文件，可以使用 json.NewEncoder 初始化文件（或者任何实现 io.Writer 的类型），并调用 Encode()；反过来与其对应的是使用 json.Decoder 和 Decode() 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func NewDecoder(r io.Reader) *Decoder
func (dec *Decoder) Decode(v interface{}) error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 Go 语言中很多标准包都实现了 Reader 和 Writer接口，因此 Encoder 和 Decoder 使用起来非常方便。&lt;/p&gt;
&lt;p&gt;例如，下面例子使用 Decode方法解码一段JSON格式数据，同时使用Encode方法将我们的结构体数据保存到文件t.json中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;encoding/json&quot;
	&quot;fmt&quot;
	&quot;os&quot;
	&quot;strings&quot;
)

type Human struct {
	name   string `json:&quot;name&quot;` // 姓名
	Gender string `json:&quot;s&quot;`    // 性别，性别的tag表明在json中为s字段
	Age    int    `json:&quot;Age&quot;`  // 年龄
	Lesson
}

type Lesson struct {
	Lessons []string `json:&quot;lessons&quot;`
}

func main() {
	// json数据的字符串
	jsonStr := `{&quot;Age&quot;: 18,&quot;name&quot;: &quot;Jim&quot; ,&quot;s&quot;: &quot;男&quot;,
	&quot;lessons&quot;:[&quot;English&quot;,&quot;History&quot;],&quot;Room&quot;:201,&quot;n&quot;:null,&quot;b&quot;:false}`
	strR := strings.NewReader(jsonStr)
	h := &amp;amp;Human{}

	// Decode 解码json数据到结构体Human中
	err := json.NewDecoder(strR).Decode(h)

	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(h)

	// 定义Encode需要的Writer
	f, err := os.Create(&quot;./t.json&quot;)

	// 把保存数据的Human结构体对象编码为json保存到文件
	json.NewEncoder(f).Encode(h)

}

程序输出：
&amp;amp;{ 男 18 {[English History]}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们调用json.NewDecoder 函数构造了 Decoder 对象，使用这个对象的 Decode方法解码给定义好的结构体对象h。对于字符串，使用 strings.NewReader 方法，让字符串变成一个 Reader。&lt;/p&gt;
&lt;p&gt;类似解码过程，我们通过json.NewEncoder()函数来构造Encoder对象，由于os中文件操作已经实现了Writer接口，所以可以直接使用，把h结构体对象编码为JSON数据格式保存在文件t.json中。&lt;/p&gt;
&lt;p&gt;文件t.json中内容为：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;{&quot;s&quot;:&quot;男&quot;,&quot;Age&quot;:18,&quot;lessons&quot;:[&quot;English&quot;,&quot;History&quot;]}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;（四）JSON数据延迟解析&lt;/p&gt;
&lt;p&gt;Human.Name字段，由于可以等到使用的时候，再根据具体数据类型来解析，因此我们可以延迟解析。当结构体Human的Name字段的类型设置为 json.RawMessage 时，它将在解码后继续以 byte 数组方式存在。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;encoding/json&quot;
	&quot;fmt&quot;
)

type Human struct {
	Name   json.RawMessage `json:&quot;name&quot;` // 姓名，json.RawMessage 类型不会进行解码
	Gender string          `json:&quot;s&quot;`    // 性别，性别的tag表明在json中为s字段
	Age    int             `json:&quot;Age&quot;`  // 年龄
	Lesson
}

type Lesson struct {
	Lessons []string `json:&quot;lessons&quot;`
}

func main() {
	jsonStr := `{&quot;Age&quot;: 18,&quot;name&quot;: &quot;Jim&quot; ,&quot;s&quot;: &quot;男&quot;,
	&quot;lessons&quot;:[&quot;English&quot;,&quot;History&quot;],&quot;Room&quot;:201,&quot;n&quot;:null,&quot;b&quot;:false}`

	var hu Human
	if err := json.Unmarshal([]byte(jsonStr), &amp;amp;hu); err == nil {
		fmt.Printf(&quot;\n 结构体Human \n&quot;)
		fmt.Printf(&quot;%+v \n&quot;, hu) // 可以看到Name字段未解码，还是字节数组
	}

	// 对延迟解码的Human.Name进行反序列化
	var UName string
	if err := json.Unmarshal(hu.Name, &amp;amp;UName); err == nil {
		fmt.Printf(&quot;\n Human.Name: %s \n&quot;, UName)
	}
}

程序输出：

 结构体Human 
{Name:[34 74 105 109 34] Gender:男 Age:18 Lesson:{Lessons:[English History]}} 

 Human.Name: Jim 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在对JSON数据第一次解码后，保存在Human的hu.Name的值还是二进制数组，在后面对hu.Name进行解码后才真正发序列化为string类型的真实字符串对象。&lt;/p&gt;
&lt;p&gt;除了Go标准库外，还有很多的第三方库也能较好解析JSON数据。这里我推荐一个第三方库：https://github.com/buger/jsonparser&lt;/p&gt;
&lt;p&gt;如同 encoding/json 包一样，在Go语言中XML也有 Marshal() 和 UnMarshal() 从 XML 中编码和解码数据；也可以从文件中读取和写入（或者任何实现了 io.Reader 和 io.Writer 接口的类型）。和 JSON 的方式一样，XML 数据可以序列化为结构，或者从结构反序列化为 XML 数据。&lt;/p&gt;
&lt;h2&gt;38.3 Protocol Buffer数据格式&lt;/h2&gt;
&lt;p&gt;Protocol Buffer 简单称为protobuf(Pb)，是Google开发出来的一个语言无关、平台无关的数据序列化工具，在rpc或tcp通信等很多场景都可以使用。在服务端定义一个数据结构，通过protobuf转化为字节流，再传送到客户端解码，就可以得到对应的数据结构。它的通信效率极高，同一条消息数据，用protobuf序列化后的大小是JSON的10分之一左右。&lt;/p&gt;
&lt;p&gt;为了正常使用protobuf，我们需要做一些准备工作。&lt;/p&gt;
&lt;p&gt;1、下载protobuf的编译器protoc，地址：https://github.com/google/protobuf/releases&lt;/p&gt;
&lt;p&gt;window用户下载: protoc-3.6.1-win32.zip，然后解压，把protoc.exe文件复制到GOPATH/bin下。
linux 用户下载：protoc-3.6.1-linux-x86_64.zip 或 protoc-3.6.1-linux-x86_32.zip，然后解压，把protoc文件复制到GOPATH/bin下。&lt;/p&gt;
&lt;p&gt;2、获取protobuf的编译器插件protoc-gen-go。&lt;/p&gt;
&lt;p&gt;在命令行运行 go get -u github.com/golang/protobuf/protoc-gen-go
会在GOPATH/bin下生成protoc-gen-go.exe文件，如果没有请自行build。GOPATH/bin目录建议加入path，以便后续操作方便。&lt;/p&gt;
&lt;p&gt;接下来我们可以正式开始尝试怎么使用protobuf了。我们需要创建一个.proto 结尾的文件，这个文件需要按照一定规则编写。&lt;/p&gt;
&lt;p&gt;具体请见官方说明：https://developers.google.com/protocol-buffers/docs/proto
也可参考：https://gowalker.org/github.com/golang/protobuf/proto&lt;/p&gt;
&lt;p&gt;protobuf的使用方法是将数据结构写入到.proto文件中，使用protoc编译器编译（通过调用protoc-gen-go）得到一个新的go包，里面包含go中可以使用的数据结构和一些辅助方法。&lt;/p&gt;
&lt;p&gt;下面我们先创建一个msg.proto文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

package learn;

message UserInfo {
    int32 UserType = 1;     //必选字段
    string UserName = 2;    //必选字段
    string UserInfo = 3;    //必选字段
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行如下命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; protoc --go_out=.  msg.proto
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会生成一个msg.pb.go的文件，代码如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Code generated by protoc-gen-go. DO NOT EDIT.
// source: msg.proto

package learn

import (
	fmt &quot;fmt&quot;
	proto &quot;github.com/golang/protobuf/proto&quot;
	math &quot;math&quot;
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package

type UserInfo struct {
	UserType             int32    `protobuf:&quot;varint,1,opt,name=UserType,proto3&quot; json:&quot;UserType,omitempty&quot;`
	UserName             string   `protobuf:&quot;bytes,2,opt,name=UserName,proto3&quot; json:&quot;UserName,omitempty&quot;`
	UserInfo             string   `protobuf:&quot;bytes,3,opt,name=UserInfo,proto3&quot; json:&quot;UserInfo,omitempty&quot;`
	XXX_NoUnkeyedLiteral struct{} `json:&quot;-&quot;`
	XXX_unrecognized     []byte   `json:&quot;-&quot;`
	XXX_sizecache        int32    `json:&quot;-&quot;`
}

func (m *UserInfo) Reset()         { *m = UserInfo{} }
func (m *UserInfo) String() string { return proto.CompactTextString(m) }
func (*UserInfo) ProtoMessage()    {}
func (*UserInfo) Descriptor() ([]byte, []int) {
	return fileDescriptor_c06e4cca6c2cc899, []int{0}
}

func (m *UserInfo) XXX_Unmarshal(b []byte) error {
	return xxx_messageInfo_UserInfo.Unmarshal(m, b)
}
func (m *UserInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
	return xxx_messageInfo_UserInfo.Marshal(b, m, deterministic)
}
func (m *UserInfo) XXX_Merge(src proto.Message) {
	xxx_messageInfo_UserInfo.Merge(m, src)
}
func (m *UserInfo) XXX_Size() int {
	return xxx_messageInfo_UserInfo.Size(m)
}
func (m *UserInfo) XXX_DiscardUnknown() {
	xxx_messageInfo_UserInfo.DiscardUnknown(m)
}

var xxx_messageInfo_UserInfo proto.InternalMessageInfo

func (m *UserInfo) GetUserType() int32 {
	if m != nil {
		return m.UserType
	}
	return 0
}

func (m *UserInfo) GetUserName() string {
	if m != nil {
		return m.UserName
	}
	return &quot;&quot;
}

func (m *UserInfo) GetUserInfo() string {
	if m != nil {
		return m.UserInfo
	}
	return &quot;&quot;
}

func init() {
	proto.RegisterType((*UserInfo)(nil), &quot;learn.UserInfo&quot;)
}

func init() { proto.RegisterFile(&quot;msg.proto&quot;, fileDescriptor_c06e4cca6c2cc899) }

var fileDescriptor_c06e4cca6c2cc899 = []byte{
	// 100 bytes of a gzipped FileDescriptorProto
	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xcc, 0x2d, 0x4e, 0xd7,
	0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xcd, 0x49, 0x4d, 0x2c, 0xca, 0x53, 0x8a, 0xe3, 0xe2,
	0x08, 0x2d, 0x4e, 0x2d, 0xf2, 0xcc, 0x4b, 0xcb, 0x17, 0x92, 0x82, 0xb0, 0x43, 0x2a, 0x0b, 0x52,
	0x25, 0x18, 0x15, 0x18, 0x35, 0x58, 0x83, 0xe0, 0x7c, 0x98, 0x9c, 0x5f, 0x62, 0x6e, 0xaa, 0x04,
	0x93, 0x02, 0xa3, 0x06, 0x67, 0x10, 0x9c, 0x0f, 0x93, 0x03, 0x99, 0x21, 0xc1, 0x8c, 0x90, 0x03,
	0xf1, 0x93, 0xd8, 0xc0, 0xb6, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x7e, 0x5d, 0xad, 0x78,
	0x7a, 0x00, 0x00, 0x00,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们在Go语言程序中使用protobuf。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;github.com/golang/protobuf/proto&quot;

	&quot;fmt&quot;
	&quot;ind/pb&quot;
)

func main() {
	//初始化message UserInfo
	usermsg := &amp;amp;pb.UserInfo{
		UserType: 1,
		UserName: &quot;Jok&quot;,
		UserInfo: &quot;I am a woker!&quot;,
	}

	//序列化
	userdata, err := proto.Marshal(usermsg)
	if err != nil {
		fmt.Println(&quot;Marshaling error: &quot;, err)
	}

	//反序列化
	encodingmsg := &amp;amp;pb.UserInfo{}
	err = proto.Unmarshal(userdata, encodingmsg)

	if err != nil {
		fmt.Println(&quot;Unmarshaling error: &quot;, err)
	}

	fmt.Printf(&quot;GetUserType: %d\n&quot;, encodingmsg.GetUserType())
	fmt.Printf(&quot;GetUserName: %s\n&quot;, encodingmsg.GetUserName())
	fmt.Printf(&quot;GetUserInfo: %s\n&quot;, encodingmsg.GetUserInfo())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：

GetUserType: 1
GetUserName: Jok
GetUserInfo: I am a woker!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过上面的介绍，我们已经学会了怎么使用protobuf来处理我们的数据。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_37_context.md&quot;&gt;第三十七章 context包&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_39_mysql.md&quot;&gt;第三十九章 Mysql数据库&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第三十七章 context包</title><link>https://blog.wemang.com/posts/go/study/42_37_context/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_37_context/</guid><pubDate>Sun, 18 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;37.1 context包&lt;/h2&gt;
&lt;p&gt;在Go中，每个请求的request在单独的协程中进行，处理一个request也可能涉及多个协程之间的交互。一个请求衍生出的各个协程之间需要满足一定的约束关系，以实现一些诸如有效期，中止routine树，传递请求全局变量之类的功能。于是Go为我们提供一个解决方案，标准context包。使用context可以使开发者方便的在这些协程之间传递request相关的数据、取消协程的signal或截止时间等。&lt;/p&gt;
&lt;p&gt;每个协程在执行之前，都要先知道程序当前的执行状态，通常将这些执行状态封装在一个Context变量中，传递给要执行的协程中。上下文则几乎已经成为传递与请求同生存周期变量的标准方法。在网络编程下，当接收到一个网络请求Request，处理Request时，我们可能需要开启不同的协程来获取数据与逻辑处理，即一个请求Request，会在多个协程中处理。而这些协程可能需要共享Request的一些信息；同时当Request被取消或者超时的时候，所有从这个Request创建的所有协程也应该被结束。&lt;/p&gt;
&lt;p&gt;context包不仅实现了在程序单元之间共享状态变量的方法，同时能通过简单的方法，使我们在被调用程序单元的外部，通过设置ctx变量值，将过期或撤销这些信号传递给被调用的程序单元。若存在A调用B的API，B再调用C的API，若A调用B取消，那也要取消B调用C，通过在A, B, C的API调用之间传递Context，以及判断其状态。&lt;/p&gt;
&lt;p&gt;Context结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Context包含过期，取消信号，request值传递等，方法在多个协程中协程安全
type Context interface {
    // Done 方法在context被取消或者超时返回一个close的channel
    Done() &amp;lt;-chan struct{}

    Err() error

    // Deadline 返回context超时时间
    Deadline() (deadline time.Time, ok bool)

    // Value 返回context相关key对应的值
    Value(key interface{}) interface{}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Deadline会返回一个超时时间，协程获得了超时时间后，例如可以对某些io操作设定超时时间。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Done方法返回一个通道（channel），当Context被撤销或过期时，该通道关闭，即它是一个表示Context是否已关闭的信号。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当Done通道关闭后，Err方法表明Context被撤的原因。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Value可以让协程共享一些数据，当然获得数据是协程安全的。但使用这些数据的时候要注意同步，比如返回了一个map，而这个map的读写则要加锁。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;协程的创建和调用关系总是像层层调用进行的，就像人的辈分一样，而更靠顶部的协程应有办法主动关闭其下属的协程的执行（不然程序可能就失控了）。为了实现这种关系，Context结构也应该像一棵树，叶子节点须总是由根节点衍生出来的。&lt;/p&gt;
&lt;p&gt;要创建Context树，第一步就是要得到根节点，context.Background函数的返回值就是根节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Background() Context
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数返回空的Context，该Context一般由接收请求的第一个协程创建，是与进入请求对应的Context根节点，它不能被取消、没有值、也没有过期时间。它常常作为处理Request的顶层context存在。&lt;/p&gt;
&lt;p&gt;有了根节点，又该怎么创建其它的子节点，孙节点呢？context包为我们提供了多个函数来创建他们：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key interface{}, val interface{}) Context
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数都接收一个Context类型的参数parent，并返回一个Context类型的值，这样就层层创建出不同的节点。子节点是从复制父节点得到的，并且根据接收参数设定子节点的一些状态值，接着就可以将子节点传递给下层的协程了。&lt;/p&gt;
&lt;p&gt;再回到之前的问题：该怎么通过Context传递改变后的状态呢？使用Context的协程无法取消某个操作，其实这也是符合常理的，因为这些协程是被某个父协程创建的，而理应只有父协程可以取消操作。在父协程中可以通过WithCancel方法获得一个cancel方法，从而获得cancel的权利。&lt;/p&gt;
&lt;p&gt;第一个WithCancel函数，它是将父节点复制到子节点，并且还返回一个额外的CancelFunc函数类型变量，该函数类型的定义为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type CancelFunc func()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用CancelFunc对象将撤销对应的Context对象，这就是主动撤销Context的方法。在父节点的Context所对应的环境中，通过WithCancel函数不仅可创建子节点的Context，同时也获得了该节点Context的控制权，一旦执行该函数，则该节点Context就结束了，则子节点需要类似如下代码来判断是否已结束，并退出该协程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select {
    case &amp;lt;-cxt.Done():
        // do some clean...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WithDeadline函数的作用也差不多，它返回的Context类型值同样是parent的副本，但其过期时间由deadline和parent的过期时间共同决定。这是因为父节点过期时，其所有的子孙节点必须同时关闭；反之，返回的父节点的过期时间则为deadline。&lt;/p&gt;
&lt;p&gt;WithTimeout函数与WithDeadline类似，不过它传入的是从现在开始Context剩余的生命时长。他们都同样也都返回了所创建的子Context的控制权，一个CancelFunc类型的函数变量。&lt;/p&gt;
&lt;p&gt;当顶层的Request请求函数结束后，我们就可以cancel掉某个context，从而再在对应协程中根据cxt.Done()来决定是否结束。&lt;/p&gt;
&lt;p&gt;WithValue函数，它返回parent的一个副本，调用该副本的Value(key)方法将得到对应key的值。这样我们不光将根节点原有的值保留了，还可以在子孙节点中加入了新的值，注意若存在Key相同，则会被覆盖。&lt;/p&gt;
&lt;p&gt;Context对象的生存周期一般仅为一个请求的处理周期。即针对一个请求创建一个Context变量（它为Context树结构的根）；在请求处理结束后，撤销此ctx变量，释放资源。&lt;/p&gt;
&lt;p&gt;每次创建一个协程时，可以将原有的Context传递给这个子协程，或者新创建一个子Context传递给这个协程。&lt;/p&gt;
&lt;p&gt;Context能灵活地存储不同类型、不同数目的值，并且使多个协程安全地读写其中的值。&lt;/p&gt;
&lt;p&gt;当通过父Context对象创建子Context对象时，即可获得子Context的一个撤销函数，这样父Context对象的创建环境就获得了对子Context的撤销权。&lt;/p&gt;
&lt;p&gt;注意：使用时遵循context规则&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;不要把Context存在一个结构体当中，显式地传入函数。Context变量需要作为第一个参数使用，一般命名为ctx。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;即使方法允许，也不要传入一个nil的Context，如果你不确定你要用什么Context的时候传一个context.TODO。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据，不要用它来传递一些可选的参数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同样的Context可以用来传递到不同的协程中，Context在多个协程中是安全的。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在子Context被传递到的协程中，应该对该子Context的Done通道（channel）进行监控，一旦该通道被关闭（即上层运行环境撤销了本协程的执行），应主动终止对当前请求信息的处理，释放资源并返回。&lt;/p&gt;
&lt;h2&gt;37.2 context应用&lt;/h2&gt;
&lt;p&gt;前面介绍协程时，对协程的管理和控制我们并没有进行讨论。到目前我们已经清楚认识了channel、context以及sync包，通过这三者，我们完全可以达到完美控制协程运行的目的。&lt;/p&gt;
&lt;p&gt;通过go关键字让我们很容易启动一个协程，但难的是很好的管理和控制他们的运行。有几种方法我们可以根据场景使用：&lt;/p&gt;
&lt;p&gt;（1）使用sync.WaitGroup，它用于线程总同步，会等待一组线程集合完成，才会继续向下执行，这对监控所有子协程全部完成情况特别有用，但要控制某个协程就无能为力了；&lt;/p&gt;
&lt;p&gt;（2）使用channel来传递消息，一个协程来发送channel信号，另一个协程通过select来得到channel信息，这种方式可以满足协程之间的通信，来控制协程运行。但如果协程数量达到一定程度，就很难把控了；或者这两个协程还和其他协程也有类似通信，比如A与B，B与C，如果A发信号B退出了，C有可能等不到B的channel信号而被遗忘；&lt;/p&gt;
&lt;p&gt;（3）使用Context来传递消息，Context是层层传递机制，根节点完全控制了子节点，根节点（父节点）可以根据需要选择自动还是手动结束子节点。而每层节点所在的协程就可以根据信息来决定下一步的操作。&lt;/p&gt;
&lt;p&gt;下面我们来看看具体使用Context怎么来控制协程的运行：&lt;/p&gt;
&lt;p&gt;这里用Context同时控制2个协程，这2个协程都可以收到cancel()发出的信号，甚至doNothing这样不结束协程可反复接收cancel信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;context&quot;
	&quot;log&quot;
	&quot;os&quot;
	&quot;time&quot;
)

var logs *log.Logger

func doClearn(ctx context.Context) {
	// for 循环来每1秒work一下，判断ctx是否被取消了，如果是就退出
	for {
		time.Sleep(1 * time.Second)
		select {
		case &amp;lt;-ctx.Done():
			logs.Println(&quot;doClearn:收到Cancel，做好收尾工作后马上退出。&quot;)
			return
		default:
			logs.Println(&quot;doClearn:每隔1秒观察信号，继续观察...&quot;)
		}
	}
}

func doNothing(ctx context.Context) {
	for {
		time.Sleep(3 * time.Second)
		select {
		case &amp;lt;-ctx.Done():
			logs.Println(&quot;doNothing:收到Cancel，但不退出......&quot;)

			// 注释return可以观察到，ctx.Done()信号是可以一直接收到的，return不注释意味退出协程
			//return
		default:
			logs.Println(&quot;doNothing:每隔3秒观察信号，一直运行&quot;)
		}
	}
}

func main() {
	logs = log.New(os.Stdout, &quot;&quot;, log.Ltime)

	// 新建一个ctx
	ctx, cancel := context.WithCancel(context.Background())

	// 传递ctx
	go doClearn(ctx)
	go doNothing(ctx)

	// 主程序阻塞20秒，留给协程来演示
	time.Sleep(20 * time.Second)
	logs.Println(&quot;cancel&quot;)

	// 调用cancel：context.WithCancel 返回的CancelFunc
	cancel()

	// 发出cancel 命令后，主程序阻塞10秒，再看协程的运行情况
	time.Sleep(10 * time.Second)
}

程序输出：
......
cancel
doClearn:收到Cancel，做好收尾工作后马上退出。
doNothing:收到Cancel，但不退出......
doNothing:收到Cancel，但不退出......
doNothing:收到Cancel，但不退出......
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里用Context嵌套控制3个协程，A，B，C。在主程序发出cancel信号后，每个协程都能接收根Context的Done()信号而退出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func A(ctx context.Context) int {
	ctx = context.WithValue(ctx, &quot;AFunction&quot;, &quot;Great&quot;)

	go B(ctx)

	select {
	// 监测自己上层的ctx ...
	case &amp;lt;-ctx.Done():
		fmt.Println(&quot;A Done&quot;)
		return -1
	}
	return 1
}

func B(ctx context.Context) int {
	fmt.Println(&quot;A value in B:&quot;, ctx.Value(&quot;AFunction&quot;))
	ctx = context.WithValue(ctx, &quot;BFunction&quot;, 999)

	go C(ctx)

	select {
	// 监测自己上层的ctx ...
	case &amp;lt;-ctx.Done():
		fmt.Println(&quot;B Done&quot;)
		return -2
	}
	return 2
}

func C(ctx context.Context) int {
	fmt.Println(&quot;B value in C:&quot;, ctx.Value(&quot;AFunction&quot;))
	fmt.Println(&quot;B value in C:&quot;, ctx.Value(&quot;BFunction&quot;))
	select {
	// 结束时候做点什么 ...
	case &amp;lt;-ctx.Done():
		fmt.Println(&quot;C Done&quot;)
		return -3
	}
	return 3
}

func main() {
	// 自动取消(定时取消)
	{
		timeout := 10 * time.Second
		ctx, _ := context.WithTimeout(context.Background(), timeout)

		fmt.Println(&quot;A 执行完成，返回：&quot;, A(ctx))
		select {
		case &amp;lt;-ctx.Done():
			fmt.Println(&quot;context Done&quot;)
			break
		}
	}
	time.Sleep(20 * time.Second)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后我们看看Context在http 是怎么传递的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;context&quot;
	&quot;net/http&quot;
	&quot;time&quot;
)

// ContextMiddle是http服务中间件，统一读取通行cookie并使用ctx传递
func ContextMiddle(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		cookie, _ := r.Cookie(&quot;Check&quot;)
		if cookie != nil {
			ctx := context.WithValue(r.Context(), &quot;Check&quot;, cookie.Value)
			next.ServeHTTP(w, r.WithContext(ctx))
		} else {
			next.ServeHTTP(w, r)
		}
	})
}

// 强制设置通行cookie
func CheckHandler(w http.ResponseWriter, r *http.Request) {
	expitation := time.Now().Add(24 * time.Hour)
	cookie := http.Cookie{Name: &quot;Check&quot;, Value: &quot;42&quot;, Expires: expitation}
	http.SetCookie(w, &amp;amp;cookie)
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
	// 通过取中间件传过来的context值来判断是否放行通过
	if chk := r.Context().Value(&quot;Check&quot;); chk == &quot;42&quot; {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(&quot;Let&apos;s go! \n&quot;))
	} else {
		w.WriteHeader(http.StatusNotFound)
		w.Write([]byte(&quot;No Pass!&quot;))
	}
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc(&quot;/&quot;, indexHandler)

	// 人为设置通行cookie
	mux.HandleFunc(&quot;/chk&quot;, CheckHandler)

	ctxMux := ContextMiddle(mux)
	http.ListenAndServe(&quot;:8080&quot;, ctxMux)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们打开浏览器访问：http://localhost:8080/chk ，然后再访问：http://localhost:8080/ ，将会看到我们正常通行后结果，否则将会看到没有正常通行下的信息。Context信息的传递主要靠中间件ContextMiddle来进行。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_36_http.md&quot;&gt;第三十六章 net/http包&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_38_json.md&quot;&gt;第三十八章 数据序列化&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第三十六章 net/http包</title><link>https://blog.wemang.com/posts/go/study/42_36_http/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_36_http/</guid><pubDate>Sat, 17 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在Go中，搭建一个HTTP server简单到令人难以置信。只需要引入net/http包，写几行代码，一个HTTP服务器就可以正常运行并接受访问请求。&lt;/p&gt;
&lt;p&gt;下面就是Go最简单的HTTP服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func myfunc(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, &quot;hi&quot;)
}

func main() {
	http.HandleFunc(&quot;/&quot;, myfunc)
	http.ListenAndServe(&quot;:8080&quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译运行程序，然后打开浏览器访问 http://localhost:8080/  , 我们可以看到网页输出&quot;hi&quot; ! 就这么简单，我们实现了一个HTTPserver！&lt;/p&gt;
&lt;p&gt;下面我们通过分析net/http的源代码，来深入理解这个包的实现原理。在net/http源代码中，我们可以深深体会到Go语言的结构体（以及自定义类型）、接口、方法简单组合的设计哲学。这个包最主要的文件有4个，分别是：
client.go
server.go
request.go
response.go&lt;/p&gt;
&lt;p&gt;这四个文件也分别代表了HTTP中最重要的4个部分，HTTP Request 请求、 HTTP Response 响应、HTTP Client客户端和HTTP Server 服务端，所以我们先从这四个方面来了解net/http包：&lt;/p&gt;
&lt;h2&gt;36.1 Request&lt;/h2&gt;
&lt;p&gt;HTTP Request请求是由客户端发出的消息, 用来使服务器执行动作.发出的消息包括起始行, Headers, Body。&lt;/p&gt;
&lt;p&gt;在net/http包中，request.go文件定义了结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Request struct 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTTP Request请求是HTTP Client客户端向HTTP Server服务端发出的消息，或者是HTTP Server服务端收到的一个请求，但是HTTP Server服务端和HTTP Client客户端使用Request时语义区别很大。我们一般使用 http.NewRequest来构造一个HTTP Request请求，可能包括HTTP Headers信息，cookies信息等，然后发给服务端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 利用指定的method, url以及可选的body返回一个新的请求.如果body参数实现了
// io.Closer接口，Request返回值的Body 字段会被设置为body，并会被Client
// 类型的Do、Post和PostForm方法以及Transport.RoundTrip方法关闭。 
func NewRequest(method, urlStr string, body io.Reader) (*Request, error) 

// 从b中读取和解析一个请求. 
func ReadRequest(b *bufio.Reader) (req *Request, err error)

// 给request添加cookie, AddCookie向请求中添加一个cookie.按照RFC 6265 
// section 5.4的规则, AddCookie不会添加超过一个Cookie头字段.
// 这表示所有的cookie都写在同一行, 用分号分隔（cookie内部用逗号分隔属性） 
func (r *Request) AddCookie(c *Cookie)

// 返回request中指定名name的cookie，如果没有发现，返回ErrNoCookie 
func (r *Request) Cookie(name string) (*Cookie, error)

// 返回该请求的所有cookies 
func (r *Request) Cookies() []*Cookie

// 利用提供的用户名和密码给http基本权限提供具有一定权限的header。
// 当使用http基本授权时，用户名和密码是不加密的 
func (r *Request) SetBasicAuth(username, password string)

// 如果在request中发送，该函数返回客户端的user-Agent
func (r *Request) UserAgent() string

// 对于指定格式的key，FormFile返回符合条件的第一个文件，如果有必要的话，
// 该函数会调用ParseMultipartForm和ParseForm。 
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)

// 返回key获取的队列中第一个值。在查询过程中post和put中的主题参数优先级
// 高于url中的value。为了访问相同key的多个值，调用ParseForm然后直接
// 检查RequestForm。 
func (r *Request) FormValue(key string) string

// 如果这是一个有多部分组成的post请求，该函数将会返回一个MIME 多部分reader，
// 否则的话将会返回一个nil和error。使用本函数代替ParseMultipartForm
// 可以将请求body当做流stream来处理。 
func (r *Request) MultipartReader() (*multipart.Reader, error)

// 解析URL中的查询字符串，并将解析结果更新到r.Form字段。对于POST或PUT
// 请求，ParseForm还会将body当作表单解析，并将结果既更新到r.PostForm也
// 更新到r.Form。解析结果中，POST或PUT请求主体要优先于URL查询字符串
// （同名变量，主体的值在查询字符串的值前面）。如果请求的主体的大小没有被
// MaxBytesReader函数设定限制，其大小默认限制为开头10MB。
// ParseMultipartForm会自动调用ParseForm。重复调用本方法是无意义的。
func (r *Request) ParseForm() error 

// ParseMultipartForm将请求的主体作为multipart/form-data解析。
// 请求的整个主体都会被解析，得到的文件记录最多 maxMemery字节保存在内存，
// 其余部分保存在硬盘的temp文件里。如果必要，ParseMultipartForm会
// 自行调用 ParseForm。重复调用本方法是无意义的。
func (r *Request) ParseMultipartForm(maxMemory int64) error 

// 返回post或者put请求body指定元素的第一个值，其中url中的参数被忽略。
func (r *Request) PostFormValue(key string) string 

// 检测在request中使用的http协议是否至少是major.minor 
func (r *Request) ProtoAtLeast(major，minor int) bool

// 如果request中有refer，那么refer返回相应的url。Referer在request
// 中是拼错的，这个错误从http初期就已经存在了。该值也可以从Headermap中
// 利用Header[&quot;Referer&quot;]获取；在使用过程中利用Referer这个方法而
// 不是map的形式的好处是在编译过程中可以检查方法的错误，而无法检查map中
// key的错误。
func (r *Request) Referer() string 

// Write方法以有线格式将HTTP/1.1请求写入w（用于将请求写入下层TCPConn等）
// 。本方法会考虑请求的如下字段：Host URL Method (defaults to &quot;GET&quot;)
//  Header ContentLength TransferEncoding Body如果存在Body，
// ContentLength字段&amp;lt;= 0且TransferEncoding字段未显式设置为
// [&quot;identity&quot;]，Write方法会显式添加”Transfer-Encoding: chunked”
// 到请求的头域。Body字段会在发送完请求后关闭。
func (r *Request) Write(w io.Writer) error 

// 该函数与Write方法类似，但是该方法写的request是按照http代理的格式去写。
// 尤其是，按照RFC 2616 Section 5.1.2，WriteProxy会使用绝对URI
// （包括协议和主机名）来初始化请求的第1行（Request-URI行）。无论何种情况，
// WriteProxy都会使用r.Host或r.URL.Host设置Host头。
func (r *Request) WriteProxy(w io.Writer) error 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;36.2 Response&lt;/h2&gt;
&lt;p&gt;HTTP Response响应是由HTTP Server服务端发出的消息，用来响应HTTP Client端发出的HTTP Request请求。发出的消息包括起始行, Headers, Body。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注意是在response.go中定义的，而在server.go有一个
// type response struct  ，注意大小写。这个结构是体现在server端的功能。
type Response struct 

// ReadResponse从r读取并返回一个HTTP 回复。req参数是可选的，指定该回复
// 对应的请求（即是对该请求的回复）。如果是nil，将假设请 求是GET请求。
// 客户端必须在结束resp.Body的读取后关闭它。读取完毕并关闭后，客户端可以
// 检查resp.Trailer字段获取回复的 trailer的键值对。
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error)

// 解析cookie并返回在header中利用set-Cookie设定的cookie值。
func (r *Response) Cookies() []*Cookie 

// 返回response中Location的header值的url。如果该值存在的话，则对于
// 请求问题可以解决相对重定向的问题，如果该值为nil，则返回ErrNOLocation。
func (r *Response) Location() (*url.URL，error) 

// 判定在response中使用的http协议是否至少是major.minor的形式。
func (r *Response) ProtoAtLeast(major, minor int) bool 

// 将response中信息按照线性格式写入w中。
func (r *Response) Write(w io.Writer) error 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;36.3 client&lt;/h2&gt;
&lt;p&gt;HTTP Client客户端主要用来发送HTTP Request请求给HTTP Server服务端，比如以Do方法，Get方法以及Post或PostForm方法发送HTTP Request请求。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Client具有Do，Get，Head，Post以及PostForm等方法。 其中Do方法可以对
// Request进行一系列的设定，而其他的对request设定较少。如果Client使用默认的
// Client，则其中的Get，Head，Post以及PostForm方法相当于默认的http.Get, 
// http.Post, http.Head以及http.PostForm函数。
type Client struct
 
// 利用GET方法对一个指定的URL进行请求，如果response是如下重定向中的一个
// 代码，则Get之后将会调用重定向内容，最多10次重定向。 
// 301 (永久重定向，告诉客户端以后应该从新地址访问) 
// 302 (暂时性重定向，作为HTTP1.0的标准，PHP的默认Location重定向用到
// 也是302)，注：303和307其实是对302的细化。 
// 303 (对于Post请求，它表示请求已经被处理，客户端可以接着使用GET方法去
// 请求Location里的URl) 
// 307 (临时重定向，对于Post请求，表示请求还没有被处理，客户端应该向
// Location里的URL重新发起Post请求)
func Get(url string) (resp *Response, err error) 

// 该函数功能见net中Head方法功能。该方法与默认的defaultClient中
// Head方法一致。
func Head(url string) (resp *Response, err error) 

// 该方法与默认的defaultClient中Post方法一致。
func Post(url string, bodyType string, body io.Reader) (resp *Response, err error)
 
// 该方法与默认的defaultClient中PostForm方法一致。 
func PostForm(url string, data url.Values) (resp *Response, err error)

// Do发送http请求并且返回一个http响应, 遵守client的策略, 如重定向, 
// cookies以及auth等.错误经常是由于策略引起的, 当err是nil时, resp
// 总会包含一个非nil的resp.body.当调用者读完resp.body之后应该关闭它, 
// 如果resp.body没有关闭, 则Client底层RoundTripper将无法重用存在的
// TCP连接去服务接下来的请求, 如果resp.body非nil, 则必须对其进行关闭.
// 通常来说, 经常使用Get, Post, 或者PostForm来替代Do. 
func (c *Client) Do(req *Request) (resp *Response, err error)

// 利用get方法请求指定的url.Get请求指定的页面信息，并返回实体主体。
func (c *Client) Get(url string) (resp *Response, err error) 

// 利用head方法请求指定的url，Head只返回页面的首部。
func (c *Client) Head(url string) (resp *Response, err error) 

// post方法请求指定的URl, 如果body也是一个io.Closer, 则在请求之后关闭它 
func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *Response, err error)

// 利用post方法请求指定的url, 利用data的key和value作为请求体. 
func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;http.NewRequest可以灵活的对http Request进行配置，然后再使用http.Client的Do方法发送这个http Request请求。注意：如果使用Post或者PostForm方法，是不能使用http.NewRequest配置请求的，只有Do方法可以定制http.NewRequest。&lt;/p&gt;
&lt;p&gt;利用http.Client以及http.NewRequest就可以完整模拟一个HTTP Request请求，包括自定义的HTTP Request请求的头部信息。有了前面介绍的 HTTP Request 请求、HTTP Response 响应、HTTP Client 客户端 三个部分，我们已经可以模拟各种HTTP Request 请求的发送，接收HTTP Response 响应了。&lt;/p&gt;
&lt;p&gt;下面我们来模拟HTTP Request请求，请求中附带有cookie信息，通过http.Client的Do方法发送这个请求。&lt;/p&gt;
&lt;p&gt;先配置http.NewRequest，然后我们通过http.Client的Do方法来发送任何HTTP Request请求。示例如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模拟任何HTTP Request请求：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;compress/gzip&quot;
	&quot;fmt&quot;
	&quot;io/ioutil&quot;
	&quot;net/http&quot;
	&quot;strconv&quot;
)

func main() {
	// 简式声明一个http.Client空结构体指针对象
	client := &amp;amp;http.Client{}

	// 使用http.NewRequest构建http Request请求
	request, err := http.NewRequest(&quot;GET&quot;, &quot;http://www.baidu.com&quot;, nil)
	if err != nil {
		fmt.Println(err)
	}

	// 使用http.Cookie结构体初始化一个cookie键值对
	cookie := &amp;amp;http.Cookie{Name: &quot;userId&quot;, Value: strconv.Itoa(12345)}

	// 使用前面构建的request方法AddCookie往请求中添加cookie
	request.AddCookie(cookie)

	// 设置request的Header，具体可参考http协议
	request.Header.Set(&quot;Accept&quot;, &quot;text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8&quot;)
	request.Header.Set(&quot;Accept-Charset&quot;, &quot;GBK, utf-8;q=0.7, *;q=0.3&quot;)
	request.Header.Set(&quot;Accept-Encoding&quot;, &quot;gzip, deflate, sdch&quot;)
	request.Header.Set(&quot;Accept-Language&quot;, &quot;zh-CN, zh;q=0.8&quot;)
	request.Header.Set(&quot;Cache-Control&quot;, &quot;max-age=0&quot;)
	request.Header.Set(&quot;Connection&quot;, &quot;keep-alive&quot;)

	// 使用http.Client 来发送request，这里使用了Do方法。
	response, err := client.Do(request)
	if err != nil {
		fmt.Println(err)
		return
	}

	// 程序结束时关闭response.Body响应流
	defer response.Body.Close()

	// 接收到的http Response 状态值
	fmt.Println(response.StatusCode)
	if response.StatusCode == 200 { // 200意味成功得到http Server返回的http Response信息

		// gzip.NewReader对压缩的返回信息解压（考虑网络传输量，http Server
	// 一般都会对响应压缩后再返回）
		body, err := gzip.NewReader(response.Body)
		if err != nil {
			fmt.Println(err)
		}

		defer body.Close()

		r, err := ioutil.ReadAll(body)
		if err != nil {
			fmt.Println(err)
		}
		// 打印出http Server返回的http Response信息
		fmt.Println(string(r))
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用http.Get 发送HTTP Get请求非常简单，在一般简单不需要对http.Request配置的场景下我们可以使用，只需要提供URL即可。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发送一个HTTP Get请求：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;io/ioutil&quot;
	&quot;net/http&quot;
)

func main() {
	// var DefaultClient = &amp;amp;Client{}
	// func Get(url string) (resp *Response, err error) {
	// return DefaultClient.Get(url)
	// }
	/*
		func (c *Client) Get(url string) (resp *Response, err error) {
			req, err := NewRequest(&quot;GET&quot;, url, nil)
			if err != nil {
				return nil, err
			}
			return c.Do(req)
		}
	*/

	// http.Get实际上是DefaultClient.Get(url)，Get函数是高度封装的，只有一个参数url。
	// 对于一般的http Request是可以使用，但是不能定制Request
	response, err := http.Get(&quot;http://www.baidu.com&quot;)
	if err != nil {
		fmt.Println(err)
	}

	//程序在使用完回复后必须关闭回复的主体。
	defer response.Body.Close()

	body, _ := ioutil.ReadAll(response.Body)
	fmt.Println(string(body))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用http.Post 发送HTTP Post请求也非常简单，在一般简单不需要对http.Request配置的场景下我们可以使用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发送一个http.Post请求：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;io/ioutil&quot;
	&quot;net/http&quot;
	&quot;strings&quot;
)

func main() {
	// application/x-www-form-urlencoded：为POST的contentType
	// strings.NewReader(&quot;mobile=xxxxxxxxxx&amp;amp;isRemberPwd=1&quot;) 理解为传递的参数
	resp, err := http.Post(&quot;http://localhost:8080/login.do&quot;,
		&quot;application/x-www-form-urlencoded&quot;, strings.NewReader(&quot;mobile=xxxxxxxxxx&amp;amp;isRemberPwd=1&quot;))
	if err != nil {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(body))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用http.PostForm 发送HTTP Request请求也非常简单，而且可以附带参数的键值对作为请求的body传递到服务端。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发送一个http.PostForm请求：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;io/ioutil&quot;
	&quot;net/http&quot;
	&quot;net/url&quot;
)

func main() {
	postParam := url.Values{
		&quot;mobile&quot;:      {&quot;xxxxxx&quot;},
		&quot;isRemberPwd&quot;: {&quot;1&quot;},
	}
	// 数据的键值会经过URL编码后作为请求的body传递
	resp, err := http.PostForm(&quot;http://localhost：8080/login.do&quot;, postParam)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(body))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面列举了四种HTTP Client客户端发送HTTP Request请求的方式，其中只有Do方法最灵活。&lt;/p&gt;
&lt;p&gt;http.Client与http.NewRequest结合可以模拟任何HTTP Request请求，方法是Do。像Get方法，Post方法和PostForm方法，http.NewRequest都是定制好的，所以使用方便但灵活性不够。不过好在有Do方法，我们可以更灵活来配置http.NewRequest。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func NewRequest(method, url string, body io.Reader) (*Request, error)

func (c *Client) Get(url string) (resp *Response, err error) {
	req, err := NewRequest(&quot;GET&quot;, url, nil)
......

func (c *Client) Post(url string, contentType string, body io.Reader) (resp *Response, err error) {
	req, err := NewRequest(&quot;POST&quot;, url, body)
......
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;36.4 HTTP Server 服务端&lt;/h2&gt;
&lt;p&gt;HTTP Server服务端用来接收并响应HTTP Client端发出的HTTP Request请求，是net/http包中非常重要和关键的一个功能。我们在Go语言中简单就能搭建HTTP服务器，就是因为它的存在。&lt;/p&gt;
&lt;p&gt;在server.go文件中还定义了一个非常重要的接口：Handler，另外还有一个结构体response，这和http.Response结构体只有首字母大小写不一致，不过这个response 也是响应，只不过是专门用在服务端，和http.Response结构体是完全两回事。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

type Server struct

// 监听TCP网络地址srv.Addr然后调用Serve来处理接下来连接的请求。
// 如果srv.Addr是空的话，则使用“:http”。
func (srv *Server) ListenAndServe() error 

// ListenAndServeTLS监听srv.Addr确定的TCP地址，并且会调用Serve
// 方法处理接收到的连接。必须提供证书文件和对应的私钥文 件。如果证书是由
// 权威机构签发的，certFile参数必须是顺序串联的服务端证书和CA证书。
// 如果srv.Addr为空字符串，会使 用”:https”。
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error 

// 接受Listener l的连接，创建一个新的服务协程。该服务协程读取请求然后调用
// srv.Handler来应答。实际上就是实现了对某个端口进行监听，然后创建相应的连接。 
func (srv *Server) Serve(l net.Listener) error

// 该函数控制是否http的keep-alives能够使用，默认情况下，keep-alives总是可用的。
// 只有资源非常紧张的环境或者服务端在关闭进程中时，才应该关闭该功能。 
func (s *Server) SetKeepAlivesEnabled(v bool)

// 是一个http请求多路复用器，它将每一个请求的URL和
// 一个注册模式的列表进行匹配，然后调用和URL最匹配的模式的处理器进行后续操作。
type ServeMux

// 初始化一个新的ServeMux 
func NewServeMux() *ServeMux

// 将handler注册为指定的模式，如果该模式已经有了handler，则会出错panic。
func (mux *ServeMux) Handle(pattern string, handler Handler) 

// 将handler注册为指定的模式 
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))

// 根据指定的r.Method, r.Host以及r.RUL.Path返回一个用来处理给定请求的handler。
// 该函数总是返回一个非nil的 handler，如果path不是一个规范格式，则handler会
// 重定向到其规范path。Handler总是返回匹配该请求的的已注册模式；在内建重定向
// 处理器的情况下，pattern会在重定向后进行匹配。如果没有已注册模式可以应用于该请求，
// 本方法将返回一个内建的”404 page not found”处理器和一个空字符串模式。
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) 

// 该函数用于将最接近请求url模式的handler分配给指定的请求。 
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Handler接口应该算是server.go中最关键的接口了，如果我们仔细看这个文件的源代码，将会发现很多结构体实现了这个接口的ServeHTTP方法。&lt;/p&gt;
&lt;p&gt;注意这个接口的注释：Handler响应HTTP请求。没错，最终我们的HTTP服务是通过实现ServeHTTP(ResponseWriter, *Request)来达到服务端接收客户端请求并响应。&lt;/p&gt;
&lt;p&gt;理解 HTTP 构建的网络应用只要关注两个端---客户端（Clinet）和服务端（Server），两个端的交互来自 Clinet 的 Request，以及Server端的Response。HTTP服务器，主要在于如何接受 Clinet端的 Request，Server端向Client端返回Response。&lt;/p&gt;
&lt;p&gt;那这个过程是什么样的呢？要讲清楚这个过程，还需要回到开始的HTTP服务器程序。这里以前面我们了解到的HTTP Request、HTTP Response、HTTP Client作为基础，并重点分析server.go源代码才能讲清楚：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
	http.HandleFunc(&quot;/&quot;, myfunc)
	http.ListenAndServe(&quot;:8080&quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上两行代码，就成功启动了一个HTTP服务器。我们通过net/http 包源代码分析发现，调用Http.HandleFunc，按顺序做了几件事：&lt;/p&gt;
&lt;p&gt;1.Http.HandleFunc调用了DefaultServeMux的HandleFunc&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.DefaultServeMux.HandleFunc调用了DefaultServeMux的Handle，DefaultServeMux是一个ServeMux 指针变量。而ServeMux 是Go语言中的Multiplexer（多路复用器），通过Handle匹配pattern 和我们定义的handler（其实就是http.HandlerFunc函数类型变量）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var DefaultServeMux = &amp;amp;defaultServeMux
var defaultServeMux ServeMux

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	mux.Handle(pattern, HandlerFunc(handler))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：
上面的方法命名Handle，HandleFunc和HandlerFunc，Handler（接口），他们很相似，容易混淆。记住Handle和HandleFunc和pattern 匹配有关，也即往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则。&lt;/p&gt;
&lt;p&gt;接着我们看看myfunc的声明和定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func myfunc(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, &quot;hi&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而type HandlerFunc func(ResponseWriter, *Request) 是一个函数类型，而我们定义的myfunc的函数签名刚好符合这个函数类型。&lt;/p&gt;
&lt;p&gt;所以http.HandleFunc(&quot;/&quot;, myfunc)，实际上是mux.Handle(&quot;/&quot;, HandlerFunc(myfunc))。&lt;/p&gt;
&lt;p&gt;HandlerFunc(myfunc) 让myfunc成为了HandlerFunc类型，我们称myfunc为handler。而HandlerFunc类型是具有ServeHTTP方法的，而有了ServeHTTP方法也就是实现了Handler接口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r) // 这相当于自身的调用
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在ServeMux和Handler都和我们的myfunc联系上了，myfunc是一个Handler接口变量也是HandlerFunc类型变量，接下来和结构体server有关了。&lt;/p&gt;
&lt;p&gt;从http.ListenAndServe的源码可以看出，它创建了一个server对象，并调用server对象的ListenAndServe方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func ListenAndServe(addr string, handler Handler) error {
    server := &amp;amp;Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而我们HTTP服务器中第二行代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http.ListenAndServe(&quot;:8080&quot;, nil)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建了一个server对象，并调用server对象的ListenAndServe方法，这里没有直接传递Handler，而是默认使用DefautServeMux作为multiplexer，myfunc是存在于handler和路由规则中的。&lt;/p&gt;
&lt;p&gt;Server的ListenAndServe方法中，会初始化监听地址Addr，同时调用Listen方法设置监听。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for {
    rw, e := l.Accept()
    ...
    c := srv.newConn(rw)
c.setState(c.rwc, StateNew) 
go c.serve(ctx)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;监听开启之后，一旦客户端请求过来，Go就开启一个协程go c.serve(ctx)处理请求，主要逻辑都在serve方法之中。&lt;/p&gt;
&lt;p&gt;func (c *conn) serve(ctx context.Context)，这个方法很长，里面主要的一句：&lt;code&gt;serverHandler{c.server}.ServeHTTP(w, w.req)&lt;/code&gt;。其中w由w, err := c.readRequest(ctx)得到，因为有传递context。&lt;/p&gt;
&lt;p&gt;还是来看源代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type serverHandler struct {
srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == &quot;&quot; &amp;amp;&amp;amp; req.Method == &quot;OPTIONS&quot; {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从http.ListenAndServe(&quot;:8080&quot;, nil)开始，handler是nil，所以最后实际ServeHTTP方法是DefaultServeMux.ServeHTTP(rw, req)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
	if r.RequestURI == &quot;*&quot; {
		if r.ProtoAtLeast(1, 1) {
			w.Header().Set(&quot;Connection&quot;, &quot;close&quot;)
		}
		w.WriteHeader(StatusBadRequest)
		return
	}
	h, _ := mux.Handler(r)
	h.ServeHTTP(w, r)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)，我们得到Handler h，然后执行h.ServeHTTP(w, r)方法，也就是执行我们的myfunc函数（别忘了myfunc是HandlerFunc类型，而他的ServeHTTP(w, r)方法这里其实就是自己调用自己），把response写到http.ResponseWriter对象返回给客户端，fmt.Fprintf(w, &quot;hi&quot;)，我们在客户端会接收到hi 。至此整个HTTP服务执行完成。&lt;/p&gt;
&lt;p&gt;总结下，HTTP服务整个过程大概是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Request -&amp;gt; ServeMux(Multiplexer) -&amp;gt; handler-&amp;gt; Response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再看下面代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http.ListenAndServe(&quot;:8080&quot;, nil)
func ListenAndServe(addr string, handler Handler) error {
    server := &amp;amp;Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码实际上就是server.ListenAndServe()执行的实际效果，只不过简单声明了一个结构体&lt;code&gt;Server{Addr: addr, Handler: handler}&lt;/code&gt;实例。如果我们声明一个Server实例，完全可以达到深度自定义 http.Server的目的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func myfunc(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, &quot;hi&quot;)
}

func main() {
	// 更多http.Server的字段可以根据情况初始化
	server := http.Server{
		Addr:         &quot;:8080&quot;,
		ReadTimeout:  0,
		WriteTimeout: 0,
	}
	http.HandleFunc(&quot;/&quot;, myfunc)
	server.ListenAndServe()
}

这样服务也能跑起来，而且我们完全可以根据情况来自定义我们的Server！

还可以指定Servemux的用法:

GOPATH\src\go42\chapter-15\15.3\7\main.go

package main

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

func myfunc(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, &quot;hi&quot;)
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc(&quot;/&quot;, myfunc)
	http.ListenAndServe(&quot;:8080&quot;, mux)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果既指定Servemux又自定义 http.Server，因为Server中有字段Handler，所以我们可以直接把Servemux变量作为Server.Handler：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func myfunc(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, &quot;hi&quot;)
}

func main() {
	server := http.Server{
		Addr:         &quot;:8080&quot;,
		ReadTimeout:  0,
		WriteTimeout: 0,
	}
	mux := http.NewServeMux()
	server.Handler = mux

	mux.HandleFunc(&quot;/&quot;, myfunc)
	server.ListenAndServe()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在前面pprof 包的内容中我们也用了本章开头这段代码，当我们访问http://localhost:8080/debug/pprof/ 时可以看到对应的性能分析报告。
因为我们这样导入 _&quot;net/http/pprof&quot; 包时，在文件 pprof.go 文件中init 函数已经定义好了handler：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func init() {
	http.HandleFunc(&quot;/debug/pprof/&quot;, Index)
	http.HandleFunc(&quot;/debug/pprof/cmdline&quot;, Cmdline)
	http.HandleFunc(&quot;/debug/pprof/profile&quot;, Profile)
	http.HandleFunc(&quot;/debug/pprof/symbol&quot;, Symbol)
	http.HandleFunc(&quot;/debug/pprof/trace&quot;, Trace)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，我们就可以通过浏览器访问上面地址来看到报告。现在再来看这些代码，我们就明白怎么回事了！&lt;/p&gt;
&lt;h2&gt;36.5 自定义处理器（Custom Handlers）&lt;/h2&gt;
&lt;p&gt;自定义的Handler：&lt;/p&gt;
&lt;p&gt;标准库http提供了Handler接口，用于开发者实现自己的handler。只要实现接口的ServeHTTP方法即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

type timeHandler struct {
	format string
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(th.format)
	w.Write([]byte(&quot;The time is: &quot; + tm))
}

func main() {
	mux := http.NewServeMux()

	th := &amp;amp;timeHandler{format: time.RFC1123}
	mux.Handle(&quot;/time&quot;, th)

	log.Println(&quot;Listening...&quot;)
	http.ListenAndServe(&quot;:3000&quot;, mux)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们知道，NewServeMux可以创建一个ServeMux实例，ServeMux同时也实现了ServeHTTP方法，因此代码中的mux也是一种handler。把它当成参数传给http.ListenAndServe方法，后者会把mux传给Server实例。因为指定了handler，因此整个http服务就不再是DefaultServeMux，而是mux，无论是在注册路由还是提供请求服务的时候。&lt;/p&gt;
&lt;p&gt;任何有 func(http.ResponseWriter，*http.Request) 签名的函数都能转化为一个 HandlerFunc 类型。这很有用，因为 HandlerFunc 对象内置了 ServeHTTP 方法，后者可以聪明又方便的调用我们最初提供的函数内容。&lt;/p&gt;
&lt;h2&gt;36.6 将函数作为处理器&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func timeHandler(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(time.RFC1123)
	w.Write([]byte(&quot;The time is: &quot; + tm))
}

func main() {
	mux := http.NewServeMux()

	// Convert the timeHandler function to a HandlerFunc type
	th := http.HandlerFunc(timeHandler)
	// And add it to the ServeMux
	mux.Handle(&quot;/time&quot;, th)

	log.Println(&quot;Listening...&quot;)
	http.ListenAndServe(&quot;:3000&quot;, mux)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建新的server：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func index(w http.ResponseWriter, r *http.Request) {
    w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)

    html := `&amp;lt;doctype html&amp;gt;
        &amp;lt;html&amp;gt;
        &amp;lt;head&amp;gt;
          &amp;lt;title&amp;gt;Hello World&amp;lt;/title&amp;gt;
        &amp;lt;/head&amp;gt;
        &amp;lt;body&amp;gt;
        &amp;lt;p&amp;gt;
          Welcome
        &amp;lt;/p&amp;gt;
        &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;`
    fmt.Fprintln(w, html)
}

func main(){
    http.HandleFunc(&quot;/&quot;, index)

    server := &amp;amp;http.Server{
        Addr: &quot;:8000&quot;, 
        ReadTimeout: 60 * time.Second, 
        WriteTimeout: 60 * time.Second, 
    }
    server.ListenAndServe()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;36.7 中间件Middleware&lt;/h2&gt;
&lt;p&gt;所谓中间件，就是连接上下级不同功能的函数或者软件，通常进行一些包裹函数的行为，为被包裹函数提供添加一些功能或行为。前文的HandleFunc就能把签名为 func(w http.ResponseWriter, r *http.Reqeust)的函数包裹成handler。这个函数也算是中间件。&lt;/p&gt;
&lt;p&gt;Go的HTTP中间件很简单，只要实现一个函数签名为func(http.Handler) http.Handler的函数即可。http.Handler是一个接口，接口方法我们熟悉的为serveHTTP。返回也是一个handler。因为Go中的函数也可以当成变量传递或者或者返回，因此也可以在中间件函数中传递定义好的函数，只要这个函数是一个handler即可，即实现或者被handlerFunc包裹成为handler处理器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func index(w http.ResponseWriter, r *http.Request) {
    w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)

    html := `&amp;lt;doctype html&amp;gt;
        &amp;lt;html&amp;gt;
        &amp;lt;head&amp;gt;
          &amp;lt;title&amp;gt;Hello World&amp;lt;/title&amp;gt;
        &amp;lt;/head&amp;gt;
        &amp;lt;body&amp;gt;
        &amp;lt;p&amp;gt;
          Welcome
        &amp;lt;/p&amp;gt;
        &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;`
    fmt.Fprintln(w, html)
}

func middlewareHandler(next http.Handler) http.Handler{
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
        // 执行handler之前的逻辑
        next.ServeHTTP(w, r)
        // 执行完毕handler后的逻辑
    })
}

func loggingHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf(&quot;Started %s %s&quot;, r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        log.Printf(&quot;Completed %s in %v&quot;, r.URL.Path, time.Since(start))
    })
}

func main() {
    http.Handle(&quot;/&quot;, loggingHandler(http.HandlerFunc(index)))

    http.ListenAndServe(&quot;:8000&quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;36.8 静态站点&lt;/h2&gt;
&lt;p&gt;下面代码通过指定目录，作为静态站点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func main() {
	http.Handle(&quot;/&quot;, http.FileServer(http.Dir(&quot;D:/html/static/&quot;)))
	http.ListenAndServe(&quot;:8080&quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_35_template.md&quot;&gt;第三十五章 模板&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_37_context.md&quot;&gt;第三十七章 context包&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第三十四章 命令行flag包 </title><link>https://blog.wemang.com/posts/go/study/42_34_flag/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_34_flag/</guid><pubDate>Fri, 16 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;34.1 命令行&lt;/h2&gt;
&lt;p&gt;写命令行程序时需要对命令参数进行解析，这时我们可以使用os库。os库可以通过变量Args来获取命令参数，os.Args返回一个字符串数组，其中第一个参数就是执行文件本身。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main
 
import (
    &quot;fmt&quot;
    &quot;os&quot;
)
 
func main() {
    fmt.Println(os.Args)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译执行后执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ./cmd -user=&quot;root&quot;
 [./cmd -user=root]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式对于简单的参数格式还能使用，一旦面对复杂的参数格式，比较费时费劲，所以这时我们会选择flag库。&lt;/p&gt;
&lt;h2&gt;34.2 flag包&lt;/h2&gt;
&lt;p&gt;Go提供了flag包，可以很方便的操作命名行参数，下面介绍下flag的用法。&lt;/p&gt;
&lt;p&gt;几个概念：&lt;/p&gt;
&lt;p&gt;1）命令行参数（或参数）：是指运行程序提供的参数&lt;/p&gt;
&lt;p&gt;2）已定义命令行参数：是指程序中通过flag.Xxx等这种形式定义了的参数&lt;/p&gt;
&lt;p&gt;3）非flag（non-flag）命令行参数（或保留的命令行参数）：先可以简单理解为flag包不能解析的参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;flag&quot;
	&quot;fmt&quot;
	&quot;os&quot;
)

var (
	h, H bool

	v bool
	q *bool

	D    string
	Conf string
)

func init() {
	flag.BoolVar(&amp;amp;h, &quot;h&quot;, false, &quot;帮助信息&quot;)
	flag.BoolVar(&amp;amp;h, &quot;H&quot;, false, &quot;帮助信息&quot;)

	flag.BoolVar(&amp;amp;v, &quot;v&quot;, false, &quot;显示版本号&quot;)

	//
	flag.StringVar(&amp;amp;D, &quot;D&quot;, &quot;deamon&quot;, &quot;set descripton &quot;)
	flag.StringVar(&amp;amp;Conf, &quot;Conf&quot;, &quot;/dev/conf/cli.conf&quot;, &quot;set Conf filename &quot;)

	// 另一种绑定方式
	q = flag.Bool(&quot;q&quot;, false, &quot;退出程序&quot;)

	// 像flag.Xxx函数格式都是一样的，第一个参数表示参数名称，
	// 第二个参数表示默认值，第三个参数表示使用说明和描述。
	// flag.XxxVar这样的函数第一个参数换成了变量地址，
        // 后面的参数和flag.Xxx是一样的。

	// 改变默认的 Usage

	flag.Usage = usage

	flag.Parse()

	var cmd string = flag.Arg(0)

	fmt.Printf(&quot;-----------------------\n&quot;)
	fmt.Printf(&quot;cli non=flags      : %s\n&quot;, cmd)

	fmt.Printf(&quot;q: %b\n&quot;, *q)

	fmt.Printf(&quot;descripton:  %s\n&quot;, D)
	fmt.Printf(&quot;Conf filename : %s\n&quot;, Conf)

	fmt.Printf(&quot;-----------------------\n&quot;)
	fmt.Printf(&quot;there are %d non-flag input param\n&quot;, flag.NArg())
	for i, param := range flag.Args() {
		fmt.Printf(&quot;#%d    :%s\n&quot;, i, param)
	}

}

func main() {
	flag.Parse()

	if h || H {
		flag.Usage()
	}
}

func usage() {
	fmt.Fprintf(os.Stderr, `CLI: 8.0
Usage: Cli [-hvq] [-D descripton] [-Conf filename] 

`)
	flag.PrintDefaults()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag包实现了命令行参数的解析，大致需要几个步骤：&lt;/p&gt;
&lt;p&gt;一：flag参数定义或绑定&lt;/p&gt;
&lt;p&gt;定义flags有两种方式：&lt;/p&gt;
&lt;p&gt;1）flag.Xxx()，其中Xxx可以是Int、String等；返回一个相应类型的指针，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var ip = flag.Int(&quot;flagname&quot;, 1234, &quot;help message for flagname&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2）flag.XxxVar()，将flag绑定到一个变量上，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var flagvar int
flag.IntVar(&amp;amp;flagvar, &quot;flagname&quot;, 1234, &quot;help message for flagname&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，还可以创建自定义flag，只要实现flag.Value接口即可（要求receiver是指针），这时候可以通过如下方式定义该flag：&lt;/p&gt;
&lt;p&gt;flag.Var(&amp;amp;flagVal, &quot;name&quot;, &quot;help message for flagname&quot;)&lt;/p&gt;
&lt;p&gt;命令行flag的语法有如下三种形式：
-flag // 只支持bool类型
-flag=x
-flag x // 只支持非bool类型&lt;/p&gt;
&lt;p&gt;二：flag参数解析&lt;/p&gt;
&lt;p&gt;在所有的flag定义完成之后，可以通过调用flag.Parse()进行解析。&lt;/p&gt;
&lt;p&gt;根据Parse()中for循环终止的条件，当parseOne返回false，nil时，Parse解析终止。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s := f.args[0]
if len(s) == 0 || s[0] != &apos;-&apos; || len(s) == 1 {
    return false, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当遇到单独的一个“-”或不是“-”开始时，会停止解析。比如：./cli – -f 或 ./cli -f&lt;/p&gt;
&lt;p&gt;这两种情况，-f都不会被正确解析。像这些参数，我们称之为non-flag参数&lt;/p&gt;
&lt;p&gt;parseOne方法中接下来是处理-flag=x，然后是-flag（bool类型）（这里对bool进行了特殊处理），接着是-flag x这种形式，最后，将解析成功的Flag实例存入FlagSet的actual map中。&lt;/p&gt;
&lt;p&gt;Arg(i int)和Args()、NArg()、NFlag()
Arg(i int)和Args()这两个方法就是获取non-flag参数的；NArg()获得non-flag个数；NFlag()获得FlagSet中actual长度（即被设置了的参数个数）。&lt;/p&gt;
&lt;p&gt;flag解析遇到non-flag参数就停止了。所以如果我们将non-flag参数放在最前面，flag什么也不会解析，因为flag遇到了这个就停止解析了。&lt;/p&gt;
&lt;p&gt;三：分支程序&lt;/p&gt;
&lt;p&gt;根据参数值，代码进入分支程序，执行相关功能。上面代码提供了 -h 参数的功能执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if h || H {
		flag.Usage()
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总体而言，从例子上看，flag package很有用，但是并没有强大到解析一切的程度。如果你的入参解析非常复杂，flag可能捉襟见肘。&lt;/p&gt;
&lt;p&gt;Cobra是一个用来创建强大的现代CLI命令行的Go开源库。开源包可能比较合适构建更为复杂的命令行程序。开源地址：https://github.com/spf13/cobra&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_33_socket.md&quot;&gt;第三十三章 Socket网络&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_35_template.md&quot;&gt;第三十五章 模板&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第三十三章 Socket网络</title><link>https://blog.wemang.com/posts/go/study/42_33_socket/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_33_socket/</guid><pubDate>Thu, 15 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;33.1 Socket基础知识&lt;/h2&gt;
&lt;p&gt;TCP/UDP、IP构成了网络通信的基石，TCP/IP是面向连接的通信协议，要求建立连接时进行3次握手确保连接已被建立，关闭连接时需要4次通信来保证客户端和服务端都已经关闭，也就是我们常说的三次握手，四次挥手。在通信过程中还有保证数据不丢失，在连接不畅通时还需要进行超时重试等等。&lt;/p&gt;
&lt;p&gt;Socket就是封装了这一套基于TCP/UDP/IP协议细节，提供了一系列套接字接口进行通信。&lt;/p&gt;
&lt;p&gt;我们知道Socket有两种：TCP Socket和UDP Socket，TCP和UDP是协议，而要确定一个进程的需要三元组，还需要IP地址和端口。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IPv4地址&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;目前的全球因特网所采用的协议族是TCP/IP协议。IP是TCP/IP协议中网络层的协议，是TCP/IP协议族的核心协议。目前主要采用的IP协议的版本号是4(简称为IPv4)，IPv4的地址位数为32位，也就是最多有2的32次方的网络设备可以联到Internet上。&lt;/p&gt;
&lt;p&gt;地址格式类似这样：127.0.0.1&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IPv6地址&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;IPv6是新一版本的互联网协议，也可以说是新一代互联网的协议，它是为了解决IPv4在实施过程中遇到的各种问题而被提出的，IPv6采用128位地址长度，几乎可以不受限制地提供地址。在IPv6的设计过程中除了一劳永逸地解决了地址短缺问题以外，还考虑了在IPv4中解决不好的其它问题，主要有端到端IP连接、服务质量（QoS）、安全性、多播、移动性、即插即用等。&lt;/p&gt;
&lt;p&gt;地址格式类似这样：2002:c0e8:82e7:0:0:0:c0e8:82e7&lt;/p&gt;
&lt;h2&gt;33.2 TCP 与 UDP&lt;/h2&gt;
&lt;p&gt;Go是自带runtime的跨平台编程语言，Go中暴露给语言使用者的TCP socket api是建立OS原生TCP socket接口之上的，所以在使用上相对简单。&lt;/p&gt;
&lt;p&gt;TCP Socket&lt;/p&gt;
&lt;p&gt;建立网络连接过程：TCP连接的建立需要经历客户端和服务端的三次握手的过程。Go 语言net包封装了系列API，在TCP连接中，服务端是一个标准的Listen + Accept的结构，而在客户端Go语言使用net.Dial或DialTimeout进行连接建立：&lt;/p&gt;
&lt;p&gt;在Go语言的net包中有一个类型TCPConn，这个类型可以用来作为客户端和服务器端交互的通道，他有两个主要的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (c *TCPConn) Write(b []byte) (n int, err os.Error)
func (c *TCPConn) Read(b []byte) (n int, err os.Error)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;TCPConn可以用在客户端和服务器端来读写数据。&lt;/p&gt;
&lt;p&gt;在Go语言中通过ResolveTCPAddr获取一个TCPAddr：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;net参数是&quot;tcp4&quot;、&quot;tcp6&quot;、&quot;tcp&quot;中的任意一个，分别表示TCP(IPv4-only), TCP(IPv6-only)或者TCP(IPv4, IPv6的任意一个)。&lt;/p&gt;
&lt;p&gt;addr表示域名或者IP地址，例如&quot;www.google.com:80&quot; 或者&quot;127.0.0.1:22&quot;。&lt;/p&gt;
&lt;p&gt;我们来看一个TCP 连接建立的具体代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// TCP server 服务端代码

package main

import (
	&quot;bufio&quot;
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;net&quot;
	&quot;time&quot;
)

func main() {

	var tcpAddr *net.TCPAddr

	tcpAddr, _ = net.ResolveTCPAddr(&quot;tcp&quot;, &quot;127.0.0.1:999&quot;)

	tcpListener, _ := net.ListenTCP(&quot;tcp&quot;, tcpAddr)

	defer tcpListener.Close()

	fmt.Println(&quot;Server ready to read ...&quot;)
	for {
		tcpConn, err := tcpListener.AcceptTCP()
		if err != nil {
			fmt.Println(&quot;accept error:&quot;, err)
			continue
		}
		fmt.Println(&quot;A client connected : &quot; + tcpConn.RemoteAddr().String())
		go tcpPipe(tcpConn)
	}

}

func tcpPipe(conn *net.TCPConn) {
	ipStr := conn.RemoteAddr().String()

	defer func() {
		fmt.Println(&quot; Disconnected : &quot; + ipStr)
		conn.Close()
	}()

	reader := bufio.NewReader(conn)
	i := 0

	for {
		message, err := reader.ReadString(&apos;\n&apos;) //将数据按照换行符进行读取。
		if err != nil || err == io.EOF {
			break
		}

		fmt.Println(string(message))

		time.Sleep(time.Second * 3)

		msg := time.Now().String() + conn.RemoteAddr().String() + &quot; Server Say hello! \n&quot;
		b := []byte(msg)

		conn.Write(b)
		i++

		if i &amp;gt; 10 {
			break
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务端 tcpListener.AcceptTCP() 接受一个客户端连接请求，通过go tcpPipe(tcpConn) 开启一个新协程来管理这对连接。 在func tcpPipe(conn *net.TCPConn)  中，处理服务端和客户端数据的交换，在这段代码for中，通过 bufio.NewReader 读取客户端发送过来的数据。&lt;/p&gt;
&lt;p&gt;客户端代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// TCP client

package main

import (
	&quot;bufio&quot;
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;net&quot;
	&quot;time&quot;
)

func main() {
	var tcpAddr *net.TCPAddr
	tcpAddr, _ = net.ResolveTCPAddr(&quot;tcp&quot;, &quot;127.0.0.1:999&quot;)

	conn, err := net.DialTCP(&quot;tcp&quot;, nil, tcpAddr)
	if err != nil {
		fmt.Println(&quot;Client connect error ! &quot; + err.Error())
		return
	}

	defer conn.Close()

	fmt.Println(conn.LocalAddr().String() + &quot; : Client connected!&quot;)

	onMessageRecived(conn)
}

func onMessageRecived(conn *net.TCPConn) {
	reader := bufio.NewReader(conn)
	b := []byte(conn.LocalAddr().String() + &quot; Say hello to Server... \n&quot;)
	conn.Write(b)
	for {
		msg, err := reader.ReadString(&apos;\n&apos;)
		fmt.Println(&quot;ReadString&quot;)
		fmt.Println(msg)

		if err != nil || err == io.EOF {
			fmt.Println(err)
			break
		}
		time.Sleep(time.Second * 2)

		fmt.Println(&quot;writing...&quot;)

		b := []byte(conn.LocalAddr().String() + &quot; write data to Server... \n&quot;)
		_, err = conn.Write(b)

		if err != nil {
			fmt.Println(err)
			break
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端net.DialTCP(&quot;tcp&quot;, nil, tcpAddr) 向服务端发起一个连接请求，调用onMessageRecived(conn)，处理客户端和服务端数据的发送与接收。在func onMessageRecived(conn *net.TCPConn) 中，通过 bufio.NewReader 读取客户端发送过来的数据。&lt;/p&gt;
&lt;p&gt;上面2个例子你可以试着运行一下，程序支持多个客户端同时运行。当然，这两个例子只是简单的TCP原始连接，在实际中，我们还可能需要定义协议。&lt;/p&gt;
&lt;p&gt;用Socket进行通信，发送的数据包一定是有结构的，类似于：数据头+数据长度+数据内容+校验码+数据尾。而在TCP流传输的过程中，可能会出现分包与黏包的现象。我们为了解决这些问题，需要我们自定义通信协议进行封包与解包。对这方面内容如有兴趣可以去了解更多相关知识。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_32_fmt.md&quot;&gt;第三十二章 fmt包与日志log包&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_34_flag.md&quot;&gt;第三十四章 命令行flag包 &lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第三十二章 fmt包与日志log包</title><link>https://blog.wemang.com/posts/go/study/42_32_fmt/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_32_fmt/</guid><pubDate>Wed, 14 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;32.1 fmt包格式化I/O&lt;/h2&gt;
&lt;p&gt;上一章我们有提到fmt格式化I/O，这一章我们就详细来说说。在fmt包，有关格式化输入输出的方法就两大类：Scan 和 Print ，分别在scan.go 和 print.go 文件中。&lt;/p&gt;
&lt;p&gt;print.go文件中定义了如下函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Printf(format string,  a ...interface{}) (n int,  err error)
func Fprintf(w io.Writer,  format string,  a ...interface{}) (n int,  err error)
func Sprintf(format string,  a ...interface{}) string

func Print(a ...interface{}) (n int,  err error)
func Fprint(w io.Writer,  a ...interface{}) (n int,  err error)
func Sprint(a ...interface{}) string

func Println(a ...interface{}) (n int,  err error)
func Fprintln(w io.Writer,  a ...interface{}) (n int,  err error)
func Sprintln(a ...interface{}) string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这9个函数，按照两个维度来说明，基本上可以说明白了。当然这两个维度是我个人为了记忆而分，并不是官方的说法。&lt;/p&gt;
&lt;p&gt;一：如果把&quot;Print&quot;理解为核心关键字，那么后面跟的后缀有&quot;f&quot;和&quot;ln&quot;以及&quot;&quot;，着重的是输出内容的最终结果；&lt;/p&gt;
&lt;p&gt;如果后缀是&quot;f&quot;, 则指定了format
如果后缀是&quot;ln&quot;, 则有换行符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Println、Fprintln、Sprintln  输出内容时会加上换行符；
Print、Fprint、Sprint        输出内容时不加上换行符；
Printf、Fprintf、Sprintf     按照指定格式化文本输出内容。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;二：如果把&quot;Print&quot;理解为核心关键字，那么前面的前缀有&quot;F&quot;和&quot;S&quot;以及&quot;&quot;，着重的是输出内容的目标（终端）；&lt;/p&gt;
&lt;p&gt;如果前缀是&quot;F&quot;, 则指定了io.Writer
如果前缀是&quot;S&quot;, 则是输出到字符串&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Print、Printf、Println      输出内容到标准输出os.Stdout；
Fprint、Fprintf、Fprintln   输出内容到指定的io.Writer；
Sprint、Sprintf、Sprintln   输出内容到字符串。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;scan.go文件中定义了如下函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Scanf(format string,  a ...interface{}) (n int,  err error)
func Fscanf(r io.Reader,  format string,  a ...interface{}) (n int,  err error)
func Sscanf(str string,  format string,  a ...interface{}) (n int,  err error)

func Scan(a ...interface{}) (n int,  err error)
func Fscan(r io.Reader,  a ...interface{}) (n int,  err error)
func Sscan(str string,  a ...interface{}) (n int,  err error)

func Scanln(a ...interface{}) (n int,  err error)
func Fscanln(r io.Reader,  a ...interface{}) (n int,  err error)
func Sscanln(str string,  a ...interface{}) (n int,  err error) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这9个函数可以扫描格式化文本以生成值。同样也可以按照两个维度来说明。&lt;/p&gt;
&lt;p&gt;一：如果把&quot;Scan&quot;理解为核心关键字，那么后面跟的后缀有&quot;f&quot;和&quot;ln&quot;以及&quot;&quot;，着重的是输入内容的结果；&lt;/p&gt;
&lt;p&gt;如果后缀是&quot;f&quot;, 则指定了format
如果后缀是&quot;ln&quot;, 则有换行符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Scanln、Fscanln、Sscanln    读取到换行时停止，并要求一次提供一行所有条目；
Scan、Fscan、Sscan          读取内容时不关注换行；
Scanf、Fscanf、Sscanf       根据格式化文本读取。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;二：如果把&quot;Scan&quot;理解为核心关键字，那么前面的前缀有&quot;F&quot;和&quot;S&quot;以及&quot;&quot;，着重的是输入内容的来源（终端）；&lt;/p&gt;
&lt;p&gt;如果前缀是&quot;F&quot;, 则指定了io.Reader
如果前缀是&quot;S&quot;, 则是从字符串读取&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Scan、Scanf、Scanln     从标准输入os.Stdin读取文本；
Fscan、Fscanf、Fscanln  从指定的io.Reader接口读取文本；
Sscan、Sscanf、Sscanln  从一个参数字符串读取文本。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;32.2 格式化verb应用&lt;/h2&gt;
&lt;p&gt;在应用上，我们主要讲讲格式化verb ，fmt包中格式化的主要功能函数都在format.go文件中。&lt;/p&gt;
&lt;p&gt;我们先来了解下有哪些verb：&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;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%v&lt;/td&gt;
&lt;td&gt;值的默认格式表示。当输出结构体时，扩展标志（%+v）会添加字段名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%#v&lt;/td&gt;
&lt;td&gt;值的Go语法表示&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%T&lt;/td&gt;
&lt;td&gt;值的类型的Go语法表示&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;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;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%t&lt;/td&gt;
&lt;td&gt;单词true或false&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%b&lt;/td&gt;
&lt;td&gt;表示为二进制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%c&lt;/td&gt;
&lt;td&gt;该值对应的unicode码值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%d&lt;/td&gt;
&lt;td&gt;表示为十进制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%o&lt;/td&gt;
&lt;td&gt;表示为八进制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%q&lt;/td&gt;
&lt;td&gt;该值对应的单引号括起来的go语法字符字面值，必要时会采用安全的转义表示&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%x&lt;/td&gt;
&lt;td&gt;表示为十六进制，使用a-f&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%X&lt;/td&gt;
&lt;td&gt;表示为十六进制，使用A-F&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%U&lt;/td&gt;
&lt;td&gt;表示为Unicode格式：U+1234，等价于&quot;U+%04X&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%b&lt;/td&gt;
&lt;td&gt;无小数部分、二进制指数的科学计数法，如-123456p-78；参见strconv.FormatFloat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%e&lt;/td&gt;
&lt;td&gt;科学计数法，如-1234.456e+78&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%E&lt;/td&gt;
&lt;td&gt;科学计数法，如-1234.456E+78&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%f&lt;/td&gt;
&lt;td&gt;有小数部分但无指数部分，如123.456&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%F&lt;/td&gt;
&lt;td&gt;等价于%f&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%g&lt;/td&gt;
&lt;td&gt;根据实际情况采用%e或%f格式（以获得更简洁、准确的输出）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%G&lt;/td&gt;
&lt;td&gt;根据实际情况采用%E或%F格式（以获得更简洁、准确的输出）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;字符串和[]byte：&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%s&lt;/td&gt;
&lt;td&gt;直接输出字符串或者[]byte&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%q&lt;/td&gt;
&lt;td&gt;该值对应的双引号括起来的Go语法字符串字面值，必要时会采用安全的转义表示&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%x&lt;/td&gt;
&lt;td&gt;每个字节用两字符十六进制数表示（使用a-f）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%X&lt;/td&gt;
&lt;td&gt;每个字节用两字符十六进制数表示（使用A-F）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%p&lt;/td&gt;
&lt;td&gt;表示为十六进制，并加上前导的0x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;宽度通过一个紧跟在百分号后面的十进制数指定，如果未指定宽度，则表示值时除必需之外不作填充。精度通过（可能有的）宽度后跟点号后跟的十进制数指定。如果未指定精度，会使用默认精度；如果点号后没有跟数字，表示精度为0。举例如下：&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;%f&lt;/td&gt;
&lt;td&gt;默认宽度，默认精度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%9f&lt;/td&gt;
&lt;td&gt;宽度9，默认精度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%.2f&lt;/td&gt;
&lt;td&gt;默认宽度，精度2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%9.2f&lt;/td&gt;
&lt;td&gt;宽度9，精度2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%9.f&lt;/td&gt;
&lt;td&gt;宽度9，精度0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对于整数，宽度和精度都设置输出总长度。采用精度时表示右对齐并用0填充，而宽度默认表示用空格填充。&lt;/p&gt;
&lt;p&gt;对于浮点数，宽度设置输出总长度；精度设置小数部分长度（如果有的话），除了%g/%G，此时精度设置总的数字个数。例如，对数字123.45，格式%6.2f 输出123.45；格式%.4g输出123.5。%e和%f的默认精度是6，%g的默认精度是可以将该值区分出来需要的最小数字个数。&lt;/p&gt;
&lt;p&gt;对复数，宽度和精度会分别用于实部和虚部，结果用小括号包裹。因此%f用于1.2+3.4i输出(1.200000+3.400000i)。&lt;/p&gt;
&lt;p&gt;其它flag：&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;/td&gt;
&lt;td&gt;总是输出数值的正负号；对%q（%+q）会生成全部是ASCII字符的输出（通过转义）；&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;tr&gt;
&lt;td&gt;#&lt;/td&gt;
&lt;td&gt;切换格式：八进制数前加0（%#o），十六进制数前加0x（%#x）或0X（%#X），指针去掉前面的0x（%#p）； 	对%q（%#q），如果strconv.CanBackquote返回真会输出反引号括起来的未转义字符串；	对%U（%#U），如果字符是可打印的，会在输出Unicode格式、空格、单引号括起来的Go字面值；&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&apos; &apos;&lt;/td&gt;
&lt;td&gt;对数值，正数前加空格而负数前加负号；对字符串采用%x或%X时（% x或% X）会给各打印的字节之间加空格；&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;使用0而不是空格填充，对于数值类型会把填充的0放在正负号后面；&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;verb会忽略不支持的旗标（flag）。&lt;/p&gt;
&lt;p&gt;下面我们用一个程序来演示下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;os&quot;
)

type User struct {
	name string
	age  int
}

var valF float64 = 32.9983
var valI int = 89
var valS string = &quot;Go is an open source programming language that makes it easy to build simple,  reliable,  and efficient software.&quot;
var valB bool = true

func main() {

	p := User{&quot;John&quot;,  28}

	fmt.Printf(&quot;Printf struct %%v : %v\n&quot;,  p)
	fmt.Printf(&quot;Printf struct %%+v : %+v\n&quot;,  p)
	fmt.Printf(&quot;Printf struct %%#v : %#v\n&quot;,  p)
	fmt.Printf(&quot;Printf struct %%T : %T\n&quot;,  p)

	fmt.Printf(&quot;Printf struct %%p : %p\n&quot;,  &amp;amp;p)

	fmt.Printf(&quot;Printf float64 %%v : %v\n&quot;,  valF)
	fmt.Printf(&quot;Printf float64 %%+v : %+v\n&quot;,  valF)
	fmt.Printf(&quot;Printf float64 %%#v : %#v\n&quot;,  valF)
	fmt.Printf(&quot;Printf float64 %%T : %T\n&quot;,  valF)
	fmt.Printf(&quot;Printf float64 %%f : %f\n&quot;,  valF)
	fmt.Printf(&quot;Printf float64 %%4.3f : %4.3f\n&quot;,  valF)
	fmt.Printf(&quot;Printf float64 %%8.3f : %8.3f\n&quot;,  valF)
	fmt.Printf(&quot;Printf float64 %%-8.3f : %-8.3f\n&quot;,  valF)
	fmt.Printf(&quot;Printf float64 %%e : %e\n&quot;,  valF)
	fmt.Printf(&quot;Printf float64 %%E : %E\n&quot;,  valF)

	fmt.Printf(&quot;Printf int %%v : %v\n&quot;,  valI)
	fmt.Printf(&quot;Printf int %%+v : %+v\n&quot;,  valI)
	fmt.Printf(&quot;Printf int %%#v : %#v\n&quot;,  valI)
	fmt.Printf(&quot;Printf int %%T : %T\n&quot;,  valI)
	fmt.Printf(&quot;Printf int %%d : %d\n&quot;,  valI)
	fmt.Printf(&quot;Printf int %%8d : %8d\n&quot;,  valI)
	fmt.Printf(&quot;Printf int %%-8d : %-8d\n&quot;,  valI)
	fmt.Printf(&quot;Printf int %%b : %b\n&quot;,  valI)
	fmt.Printf(&quot;Printf int %%c : %c\n&quot;,  valI)
	fmt.Printf(&quot;Printf int %%o : %o\n&quot;,  valI)
	fmt.Printf(&quot;Printf int %%U : %U\n&quot;,  valI)
	fmt.Printf(&quot;Printf int %%q : %q\n&quot;,  valI)
	fmt.Printf(&quot;Printf int %%x : %x\n&quot;,  valI)

	fmt.Printf(&quot;Printf string %%v : %v\n&quot;,  valS)
	fmt.Printf(&quot;Printf string %%+v : %+v\n&quot;,  valS)
	fmt.Printf(&quot;Printf string %%#v : %#v\n&quot;,  valS)
	fmt.Printf(&quot;Printf string %%T : %T\n&quot;,  valS)
	fmt.Printf(&quot;Printf string %%x : %x\n&quot;,  valS)
	fmt.Printf(&quot;Printf string %%X : %X\n&quot;,  valS)
	fmt.Printf(&quot;Printf string %%s : %s\n&quot;,  valS)
	fmt.Printf(&quot;Printf string %%200s : %200s\n&quot;,  valS)
	fmt.Printf(&quot;Printf string %%-200s : %-200s\n&quot;,  valS)
	fmt.Printf(&quot;Printf string %%q : %q\n&quot;,  valS)

	fmt.Printf(&quot;Printf bool %%v : %v\n&quot;,  valB)
	fmt.Printf(&quot;Printf bool %%+v : %+v\n&quot;,  valB)
	fmt.Printf(&quot;Printf bool %%#v : %#v\n&quot;,  valB)
	fmt.Printf(&quot;Printf bool %%T : %T\n&quot;,  valB)
	fmt.Printf(&quot;Printf bool %%t : %t\n&quot;,  valB)

	s := fmt.Sprintf(&quot;a %s&quot;,  &quot;string&quot;)
	fmt.Println(s)

	fmt.Fprintf(os.Stderr,  &quot;an %s\n&quot;,  &quot;error&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：

Printf struct %v : {John 28}
Printf struct %+v : {name:John age:28}
Printf struct %#v : main.User{name:&quot;John&quot;, age:28}
Printf struct %T : main.User
Printf struct %p : 0xc000048400
Printf float64 %v : 32.9983
Printf float64 %+v : 32.9983
Printf float64 %#v : 32.9983
Printf float64 %T : float64
Printf float64 %f : 32.998300
Printf float64 %4.3f : 32.998
Printf float64 %8.3f :   32.998
Printf float64 %-8.3f : 32.998  
Printf float64 %e : 3.299830e+01
Printf float64 %E : 3.299830E+01
Printf int %v : 89
Printf int %+v : 89
Printf int %#v : 89
Printf int %T : int
Printf int %d : 89
Printf int %8d :       89
Printf int %-8d : 89      
Printf int %b : 1011001
Printf int %c : Y
Printf int %o : 131
Printf int %U : U+0059
Printf int %q : &apos;Y&apos;
Printf int %x : 59
Printf string %v : Go is an open source programming language that makes it easy to build simple,  reliable,  and efficient software.
Printf string %+v : Go is an open source programming language that makes it easy to build simple,  reliable,  and efficient software.
Printf string %#v : &quot;Go is an open source programming language that makes it easy to build simple,  reliable,  and efficient software.&quot;
Printf string %T : string
Printf string %x : 476f20697320616e206f70656e20736f757263652070726f6772616d6d696e67206c616e67756167652074686174206d616b6573206974206561737920746f206275696c642073696d706c652c202072656c6961626c652c2020616e6420656666696369656e7420736f6674776172652e
Printf string %X : 476F20697320616E206F70656E20736F757263652070726F6772616D6D696E67206C616E67756167652074686174206D616B6573206974206561737920746F206275696C642073696D706C652C202072656C6961626C652C2020616E6420656666696369656E7420736F6674776172652E
Printf string %s : Go is an open source programming language that makes it easy to build simple,  reliable,  and efficient software.
Printf string %200s :                                                                                        Go is an open source programming language that makes it easy to build simple,  reliable,  and efficient software.
Printf string %-200s : Go is an open source programming language that makes it easy to build simple,  reliable,  and efficient software.                                                                                       
Printf string %q : &quot;Go is an open source programming language that makes it easy to build simple,  reliable,  and efficient software.&quot;
Printf bool %v : true
Printf bool %+v : true
Printf bool %#v : true
Printf bool %T : bool
Printf bool %t : true
a string
an error

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们主要通过fmt.Printf来理解这些flag 的含义，这对我们今后的开发有较强的实际作用。至于其他函数，我就不一一举例，有兴趣可以进一步研究。&lt;/p&gt;
&lt;h2&gt;32.3 日志log包&lt;/h2&gt;
&lt;p&gt;Go语言标准包中有日志功能，对应在log包中。主要结构体是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Logger struct {
	mu     sync.Mutex // ensures atomic writes; protects the following fields
	prefix string     // prefix to write at beginning of each line
	flag   int        // properties
	out    io.Writer  // destination for output
	buf    []byte     // for accumulating text to write
}


func New(out io.Writer, prefix string, flag int) *Logger {
	return &amp;amp;Logger{out: out, prefix: prefix, flag: flag}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在log包中通过New函数得到一个Logger结构体指针，这个函数的三个参数分别是out，prefix，flag。pfefix可以指定日志信息的前缀，比如“[Debug]”等，一般根据实际需要定义，可根据情况随时通过SetPrefix()函数修改。flag是日志的前缀信息（在prefix之后），包括可配置的时间格式等，一般默认为LstdFlags就可以了。out是日志输出的目标，只要实现了io.Writer接口就可以作为out，log包中默认指定stderr为out，所以log包默认都是输出到标准设备。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var std = New(os.Stderr, &quot;&quot;, LstdFlags)

func Println(v ...interface{}) {
	std.Output(2, fmt.Sprintln(v...))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以按照上面的思路，把日志信息写入到文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logfile, err := os.OpenFile(&quot;my.log&quot;, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
	log.Fatalln(&quot;fail to create log file!&quot;)
}
defer logfile.Close()

l:=log.New(logfile, &quot;&quot;, log.LstdFlags)
l.Println(&quot;test&quot;)
num:=5
l.Println(&quot;test %d&quot;,num)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为logfile已经实现了io.Writer，所以这里用做out，日志信息被写入到文件。log的方法Printf()可以把信息按照一定格式来写入。另外，在写入日志信息时都有加入并发锁，这是mu  sync.Mutex的作用。&lt;/p&gt;
&lt;p&gt;最后，log包的日志功能基本上能满足一般的开发需要，但相对还是比较简单，缺少日志分层控制，缺少对json格式的支持等，所以如果有需要灵活定制或大并发、大吞吐量的日志开发需求，建议考虑使用其他方法或途径来实现。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_31_io.md&quot;&gt;第三十一章 文件操作与IO&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_33_socket.md&quot;&gt;第三十三章 Socket网络&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第三十一章 文件操作与I/O</title><link>https://blog.wemang.com/posts/go/study/42_31_io/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_31_io/</guid><pubDate>Tue, 13 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;31.1 文件系统&lt;/h2&gt;
&lt;p&gt;对于文件和目录的操作，Go主要在os包中提供了的相应方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Mkdir(name string, perm FileMode) error 
func Chdir(dir string) error
func TempDir() string
func Rename(oldpath, newpath string) error
func Chmod(name string, mode FileMode) error
func Open(name string) (*File, error) {
	return OpenFile(name, O_RDONLY, 0)
}
func Create(name string) (*File, error) {
	return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
	testlog.Open(name)
	return openFileNolog(name, flag, perm)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面函数定义中我们可以发现一个情况：那就是os包中不同函数打开（创建）文件的操作，最终还是通过函数OpenFile()来实现，而OpenFile()由编译器根据系统的情况来选择不同的底层功能来实现，对这个实现细节有兴趣可以根据标准包来仔细了解，这里就不展开讲了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;os.Open(name string) 使用只读模式打开文件；
os.Create(name string) 创建新文件，如文件存在则原文件内容会丢失；
os.OpenFile(name string, flag int, perm FileMode) 这个函数可以指定flag和FileMode 。这三个函数都会返回一个文件对象。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预定义的Flag值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;O_RDONLY int = syscall.O_RDONLY // 只读打开文件和os.Open()同义
O_WRONLY int = syscall.O_WRONLY // 只写打开文件	
O_RDWR   int = syscall.O_RDWR   // 读写方式打开文件	
O_APPEND int = syscall.O_APPEND // 当写的时候使用追加模式到文件末尾	
O_CREATE int = syscall.O_CREAT  // 如果文件不存在，此案创建	
O_EXCL   int = syscall.O_EXCL   // 和O_CREATE一起使用，只有当文件不存在时才创建
O_SYNC   int = syscall.O_SYNC   // 以同步I/O方式打开文件，直接写入硬盘
O_TRUNC  int = syscall.O_TRUNC  // 如果可以的话，当打开文件时先清空文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在ioutil包中，也可以对文件操作，主要有下面三个函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func ReadFile(filename string) ([]byte, error) // f, err := os.Open(filename)
func WriteFile(filename string, data []byte, perm os.FileMode) error  //os.OpenFile
func ReadDir(dirname string) ([]os.FileInfo, error) //	f, err := os.Open(dirname)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这三个函数涉及到了文件I/O ，而对文件的操作我们除了打开（创建），关闭外，更主要的是对内容的读写操作上，也即是文件I/O处理上。在Go语言中，对于I/O的操作在Go 语言很多标准库中存在，很难完整地讲清楚。下面我就尝试结合io, ioutil, bufio这三个标准库，讲一讲这几个标准库在文件I/O操作中的具体使用方法。&lt;/p&gt;
&lt;h2&gt;31.2 I/O读写&lt;/h2&gt;
&lt;p&gt;Go 语言中，为了方便开发者使用，将 I/O 操作封装在了大概如下几个包中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;io 为 I/O 原语（I/O primitives）提供基本的接口&lt;/li&gt;
&lt;li&gt;io/ioutil 封装一些实用的 I/O 函数&lt;/li&gt;
&lt;li&gt;fmt 实现格式化 I/O，类似 C 语言中的 printf 和 scanf ，后面会详细讲解&lt;/li&gt;
&lt;li&gt;bufio 实现带缓冲I/O&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 io 包中最重要的是两个接口：Reader 和 Writer 接口。&lt;/p&gt;
&lt;p&gt;这两个接口是我们了解整个I/O的关键，我们只要记住：&lt;strong&gt;实现了这两个接口，就有了 I/O 的功能&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;有关缓冲：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;内核中的缓冲：无论进程是否提供缓冲，内核都是提供缓冲的，系统对磁盘的读写都会提供一个缓冲（内核高速缓冲），将数据写入到块缓冲进行排队，当块缓冲达到一定的量时，才把数据写入磁盘。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程中的缓冲：是指对输入输出流进行了改进，提供了一个流缓冲，当调用一个函数向磁盘写数据时，先把数据写入缓冲区，当达到某个条件，如流缓冲满了，或刷新流缓冲，这时候才会把数据一次送往内核提供的块缓冲中，再经块缓冲写入磁盘。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go 语言提供了很多读写文件的方式，一般来说常用的有三种。
一：os.File 实现了Reader 和 Writer 接口，所以在文件对象上，我们可以直接读写文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (f *File) Read(b []byte) (n int, err error)
func (f *File) Write(b []byte) (n int, err error)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在使用File.Read读文件时，可考虑使用buffer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;os&quot;
)

func main() {
	b := make([]byte, 1024)
	f, err := os.Open(&quot;./tt.txt&quot;)
	_, err = f.Read(b)
	f.Close()

	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(string(b))

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;二：ioutil库，没有直接实现Reader 和 Writer 接口，但是通过内部调用，也可读写文件内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func ReadAll(r io.Reader) ([]byte, error) 
func ReadFile(filename string) ([]byte, error)  //os.Open
func WriteFile(filename string, data []byte, perm os.FileMode) error  //os.OpenFile
func ReadDir(dirname string) ([]os.FileInfo, error)  // os.Open
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三：使用bufio库，这个库实现了I/O的缓冲操作，通过内嵌io.Reader、io.Writer接口，新建了Reader ，Writer 结构体。同时也实现了Reader 和 Writer 接口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Reader struct {
	buf          []byte
	rd           io.Reader // reader provided by the client
	r, w         int       // buf read and write positions
	err          error
	lastByte     int
	lastRuneSize int
}

type Writer struct {
	err error
	buf []byte
	n   int
	wr  io.Writer
}


func (b *Reader) Read(p []byte) (n int, err error) 
func (b *Writer) Write(p []byte) (nn int, err error) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这三种读方式的效率怎么样呢，我们可以看看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;bufio&quot;
	&quot;fmt&quot;
	&quot;io&quot;
	&quot;io/ioutil&quot;
	&quot;os&quot;
	&quot;time&quot;
)

func read1(path string) {
	fi, err := os.Open(path)
	if err != nil {
		panic(err)
	}
	defer fi.Close()
	buf := make([]byte, 1024)
	for {
		n, err := fi.Read(buf)
		if err != nil &amp;amp;&amp;amp; err != io.EOF {
			panic(err)
		}
		if 0 == n {
			break
		}
	}
}

func read2(path string) {
	fi, err := os.Open(path)
	if err != nil {
		panic(err)
	}
	defer fi.Close()
	r := bufio.NewReader(fi)
	buf := make([]byte, 1024)
	for {
		n, err := r.Read(buf)
		if err != nil &amp;amp;&amp;amp; err != io.EOF {
			panic(err)
		}
		if 0 == n {
			break
		}
	}
}

func read3(path string) {
	fi, err := os.Open(path)
	if err != nil {
		panic(err)
	}
	defer fi.Close()
	_, err = ioutil.ReadAll(fi)
}

func main() {

	file := &quot;&quot; //找一个大的文件，如日志文件
	start := time.Now()
	read1(file)
	t1 := time.Now()
	fmt.Printf(&quot;Cost time %v\n&quot;, t1.Sub(start))
	read2(file)
	t2 := time.Now()
	fmt.Printf(&quot;Cost time %v\n&quot;, t2.Sub(t1))
	read3(file)
	t3 := time.Now()
	fmt.Printf(&quot;Cost time %v\n&quot;, t3.Sub(t2))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过多次测试，基本上保持 file.Read &amp;gt; ioutil &amp;gt;bufio 这样的成绩， bufio读同一文件耗费时间最少，效果稳稳地保持在最佳。&lt;/p&gt;
&lt;h2&gt;31.3 ioutil包&lt;/h2&gt;
&lt;p&gt;下面代码使用ioutil包实现2种读文件，1种写文件的方法，其中 ioutil.ReadAll 可以读取所有io.Reader流。所以在网络连接中，也经常使用ioutil.ReadAll来读取流，后面章节我们会讲到这块内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;io/ioutil&quot;
	&quot;os&quot;
)

func main() {
	fileObj, err := os.Open(&quot;./tt.txt&quot;)
	defer fileObj.Close()

	Contents, _ := ioutil.ReadAll(fileObj)
	fmt.Println(string(contents))

	if contents, _ := ioutil.ReadFile(&quot;./tt.txt&quot;); err == nil {
		fmt.Println(string(contents))
	}

	ioutil.WriteFile(&quot;./t3.txt&quot;, contents, 0666)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;31.4 bufio包&lt;/h2&gt;
&lt;p&gt;bufio 包通过 bufio.NewReader 和bufio.NewWriter 来创建I/O方法集，利用缓冲来处理流，后面章节我们也会讲到这块内容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;bufio&quot;
	&quot;fmt&quot;
	&quot;os&quot;
)

func main() {
	fileObj, _ := os.OpenFile(&quot;./tt.txt&quot;, os.O_RDWR|os.O_CREATE, 0666)
	defer fileObj.Close()

	Rd := bufio.NewReader(fileObj)
	cont, _ := Rd.ReadSlice(&apos;#&apos;)
	fmt.Println(string(cont))

	Wr := bufio.NewWriter(fileObj)
	Wr.WriteString(&quot;WriteString writes a ## string.&quot;)
	Wr.Flush()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：
WriteString writes a #
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;bufio包中，主要方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// NewReaderSize 将 rd 封装成一个带缓存的 bufio.Reader 对象，缓存大小由 size 指定（如果小于 16 则会被设置为 16）。
func NewReaderSize(rd io.Reader, size int) *Reader

// NewReader 相当于 NewReaderSize(rd, 4096)
func NewReader(rd io.Reader) *Reader

// Peek 返回缓存的一个切片，该切片引用缓存中前 n 个字节的数据。 
// 如果 n 大于缓存的总大小，则返回 当前缓存中能读到的字节的数据。
func (b *Reader) Peek(n int) ([]byte, error)


// Read 从 b 中读出数据到 p 中，返回读出的字节数和遇到的错误。
// 如果缓存不为空，则只能读出缓存中的数据，不会从底层 io.Reader 
// 中提取数据，如果缓存为空，则：
// 1、len(p) &amp;gt;= 缓存大小，则跳过缓存，直接从底层 io.Reader 中读出到 p 中。
// 2、len(p) &amp;lt; 缓存大小，则先将数据从底层 io.Reader 中读取到缓存中，
// 再从缓存读取到 p 中。
func (b *Reader) Read(p []byte) (n int, err error)

// Buffered 该方法返回从当前缓存中能被读到的字节数。
func (b *Reader) Buffered() int

// Discard 方法跳过后续的 n 个字节的数据，返回跳过的字节数。
func (b *Reader) Discard(n int) (discarded int, err error)

// ReadSlice 在 b 中查找 delim 并返回 delim 及其之前的所有数据。
// 该操作会读出数据，返回的切片是已读出的数据的引用，切片中的数据在下一次
// 读取操作之前是有效的。
// 如果找到 delim，则返回查找结果，err 返回 nil。
// 如果未找到 delim，则：
// 1、缓存不满，则将缓存填满后再次查找。
// 2、缓存是满的，则返回整个缓存，err 返回 ErrBufferFull。
// 如果未找到 delim 且遇到错误（通常是 io.EOF），则返回缓存中的所有数据
// 和遇到的错误。
// 因为返回的数据有可能被下一次的读写操作修改，所以大多数操作应该使用 
// ReadBytes 或 ReadString，它们返回的是数据的拷贝。
func (b *Reader) ReadSlice(delim byte) (line []byte, err error)

// ReadLine 是一个低水平的行读取原语，大多数情况下，应该使用ReadBytes(&apos;\n&apos;)
//  或 ReadString(&apos;\n&apos;)，或者使用一个 Scanner。
// ReadLine 通过调用 ReadSlice 方法实现，返回的也是缓存的切片。
// 用于读取一行数据，不包括行尾标记（\n 或 \r\n）。
// 只要能读出数据，err 就为 nil。如果没有数据可读，则 isPrefix 
// 返回 false，err 返回 io.EOF。
// 如果找到行尾标记，则返回查找结果，isPrefix 返回 false。
// 如果未找到行尾标记，则：
// 1、缓存不满，则将缓存填满后再次查找。
// 2、缓存是满的，则返回整个缓存，isPrefix 返回 true。
// 整个数据尾部“有一个换行标记”和“没有换行标记”的读取结果是一样。
// 如果 ReadLine 读取到换行标记，则调用 UnreadByte 撤销的是换行标记，
// 而不是返回的数据。
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)

// ReadBytes 功能同 ReadSlice，只不过返回的是缓存的拷贝。
func (b *Reader) ReadBytes(delim byte) (line []byte, err error)

// ReadString 功能同 ReadBytes，只不过返回的是字符串。
func (b *Reader) ReadString(delim byte) (line string, err error)

// Reset 将 b 的底层 Reader 重新指定为 r，同时丢弃缓存中的所有数据，
// 复位所有标记和错误信息。 bufio.Reader。
func (b *Reader) Reset(r io.Reader)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面一段代码是，里面有用到peek，Discard 等方法，可以修改方法参数值，仔细体会：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;bufio&quot;
	&quot;fmt&quot;
	&quot;strings&quot;
)

func main() {
	sr := strings.NewReader(&quot;ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890&quot;)
	buf := bufio.NewReaderSize(sr, 0) //默认16
	b := make([]byte, 10)

	fmt.Println(&quot;==&quot;, buf.Buffered()) // 0
	S, _ := buf.Peek(5)
	fmt.Printf(&quot;%d ==  %q\n&quot;, buf.Buffered(), s) // 
	nn, er := buf.Discard(3)
	fmt.Println(nn, er)

	for n, err := 0, error(nil); err == nil; {
		fmt.Printf(&quot;Buffered:%d ==Size:%d== n:%d==  b[:n] %q ==  err:%v\n&quot;, buf.Buffered(), buf.Size(), n, b[:n], err)
		n, err = buf.Read(b)
		fmt.Printf(&quot;Buffered:%d ==Size:%d== n:%d==  b[:n] %q ==  err: %v == s: %s\n&quot;, buf.Buffered(), buf.Size(), n, b[:n], err, s)
	}

	fmt.Printf(&quot;%d ==  %q\n&quot;, buf.Buffered(), s)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有关I/O 的处理，这里主要讲了针对文件的处理。后面在网络I/O读写处理中，我们将会接触到更多的方式和方法。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_30_os.md&quot;&gt;第三十章 OS包&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_32_fmt.md&quot;&gt;第三十二章 fmt包与日志log包&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>《Go语言四十二章经》第二十九章 排序(sort)</title><link>https://blog.wemang.com/posts/go/study/42_29_sort/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_29_sort/</guid><pubDate>Mon, 12 Feb 2024 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;《Go语言四十二章经》第二十九章 排序(sort)&lt;/h1&gt;
&lt;h2&gt;29.1 sort包介绍&lt;/h2&gt;
&lt;p&gt;Go语言标准库sort包中实现了几种基本的排序算法：插入排序、快排和堆排序，但在使用sort包进行排序时无需具体考虑使用那种排序方式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func insertionSort(data Interface, a, b int) 
func heapSort(data Interface, a, b int)
func quickSort(data Interface, a, b, maxDepth int) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;sort.Interface接口定义了三个方法，注意sort包中接口Interface这个名字，是大写字母I开头，不要和interface关键字混淆，这里就是一个接口名而已。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Interface interface {
	// Len 为集合内元素的总数  
	Len() int
	// 如果index为i的元素小于index为j的元素，则返回true，否则false
	Less(i, j int) bool
	// Swap 交换索引为 i 和 j 的元素
	Swap(i, j int)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这三个方法分别是：获取数据集合长度的Len()方法、比较两个元素大小的Less()方法和交换两个元素位置的Swap()方法。只要实现了这三个方法，就可以对数据集合进行排序，sort包会根据实际数据自动选择高效的排序算法。&lt;/p&gt;
&lt;p&gt;sort包原生支持[]int、[]float64和[]string三种内建数据类型切片的排序操作，即不必我们自己实现相关的Len()、Less()和Swap()方法。&lt;/p&gt;
&lt;p&gt;以[]int为例，我们看看在sort包中的是怎么定义排序操作的:&lt;/p&gt;
&lt;p&gt;type IntSlice []int&lt;/p&gt;
&lt;p&gt;先通过 []int 来定义新类型IntSlice，然后在IntSlice上定义三个方法，Len()，Less(i, j int)，Swap(i, j int)，实现了这三个方法也就意味着实现了sort.Interface。&lt;/p&gt;
&lt;p&gt;方法 func (p IntSlice) Sort() 通过调用 sort.Sort(p) 函数来实现排序。而p因为是sort.Interface类型，但IntSlice实现了这三个接口方法，也是sort.Interface类型，因此可以直接调用得到排序结果。其他[]float64和[]string的排序也基本上按照这种方式来实现。&lt;/p&gt;
&lt;p&gt;其他类型并没有在标准包中给出实现方法，需要我们自己来定义实现。下面第二节 自定义sort.Interface排序 就是专门来讲怎么实现的，但有了这三个实现的实例，自定义实现排序也就很容易了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (p IntSlice) Len() int           { return len(p) }
func (p IntSlice) Less(i, j int) bool { return p[i] &amp;lt; p[j] }
func (p IntSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
func (p IntSlice) Sort() { Sort(p) }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来看看[]int，[]string排序的实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;sort&quot;
)

func main() {
	a := []int{3, 5, 4, -1, 9, 11, -14}
	sort.Ints(a)
	fmt.Println(a)
	ss := []string{&quot;surface&quot;, &quot;ipad&quot;, &quot;mac pro&quot;, &quot;mac air&quot;, &quot;think pad&quot;, &quot;idea pad&quot;}
	sort.Strings(ss)
	fmt.Println(ss)
	sort.Sort(sort.Reverse(sort.StringSlice(ss)))
	fmt.Printf(&quot;After reverse: %v\n&quot;, ss)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：
[-14 -1 3 4 5 9 11]
[idea pad ipad mac air mac pro surface think pad]
After reverse: [think pad surface mac pro mac air ipad idea pad]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认结果都是升序排列，如果我们想对一个 sortable object 进行逆序排序，可以自定义一个type。但 sort.Reverse 帮你省掉了这些代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;sort&quot;
)

func main() {
	a := []int{4, 3, 2, 1, 5, 9, 8, 7, 6}
	sort.Sort(sort.Reverse(sort.IntSlice(a)))
	fmt.Println(&quot;After reversed: &quot;, a)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：
After reversed:  [9 8 7 6 5 4 3 2 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相关方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 将类型为float64的slice以升序方式排序
func Float64s(a []float64)   

// 判定是否已经进行排序func Ints(a []int)
func Float64sAreSorted(a []float64) bool　

// Ints 以升序排列 int 切片。
func Ints(a []int)                  

// 判断 int 切片是否已经按升序排列。
func IntsAreSorted(a []int) bool　

//IsSorted 判断数据是否已经排序。包括各种可sort的数据类型的判断．
func IsSorted(data Interface) bool    


//Strings 以升序排列 string 切片。
func Strings(a []string)

//判断 string 切片是否按升序排列
func StringsAreSorted(a []string) bool

// search使用二分法进行查找，Search()方法回使用“二分查找”算法来搜索某指定切片[0:n]，
// 并返回能够使f(i)=true的最小的i（0&amp;lt;=i&amp;lt;n）值，并且会假定，如果f(i)=true，则f(i+1)=true，
// 即对于切片[0:n]，i之前的切片元素会使f()函数返回false，i及i之后的元素会使f()
// 函数返回true。但是，当在切片中无法找到时f(i)=true的i时（此时切片元素都不能使f()
// 函数返回true），Search()方法会返回n（而不是返回-1）。
//
// Search 常用于在一个已排序的，可索引的数据结构中寻找索引为 i 的值 x，例如数组或切片。
// 这种情况下实参 f一般是一个闭包，会捕获所要搜索的值，以及索引并排序该数据结构的方式。
func Search(n int, f func(int) bool) int   

// SearchFloat64s 在float64s切片中搜索x并返回索引如Search函数所述. 
// 返回可以插入x值的索引位置，如果x不存在，返回数组a的长度切片必须以升序排列
func SearchFloat64s(a []float64, x float64) int　　

// SearchInts 在ints切片中搜索x并返回索引如Search函数所述. 返回可以插入x值的
// 索引位置，如果x不存在，返回数组a的长度切片必须以升序排列
func SearchInts(a []int, x int) int 

// SearchFloat64s 在strings切片中搜索x并返回索引如Search函数所述. 返回可以
// 插入x值的索引位置，如果x不存在，返回数组a的长度切片必须以升序排列
func SearchStrings(a []string, x string) int

// 其中需要注意的是，以上三种search查找方法，其对应的slice必须按照升序进行排序，
// 否则会出现奇怪的结果．

// Sort 对 data 进行排序。它调用一次 data.Len 来决定排序的长度 n，调用 data.Less 
// 和 data.Swap 的开销为O(n*log(n))。此排序为不稳定排序。他根据不同形式决定使用
// 不同的排序方式（插入排序，堆排序，快排）。
func Sort(data Interface)

// Stable对data进行排序，不过排序过程中，如果data中存在相等的元素，则他们原来的
// 顺序不会改变，即如果有两个相等元素num, 他们的初始index分别为i和j，并且i&amp;lt;j，
// 则利用Stable对data进行排序后，i依然小于ｊ．直接利用sort进行排序则不能够保证这一点。
func Stable(data Interface)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;29.2 自定义sort.Interface排序&lt;/h2&gt;
&lt;p&gt;如果是具体的某个结构体的排序，就需要自己实现Interface了。数据集合（包括自定义数据类型的集合）排序需要实现sort.Interface接口的三个方法，即：Len()，Swap(i, j int)，Less(i, j int)，数据集合实现了这三个方法后，即可调用该包的Sort()方法进行排序。Sort(data Interface) 方法内部会使用quickSort()来进行集合的排序。quickSort()会根据实际情况来选择排序方法。&lt;/p&gt;
&lt;p&gt;任何实现了 sort.Interface 的类型（一般为集合），均可使用该包中的方法进行排序。这些方法要求集合内列出元素的索引为整数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;sort&quot;
)

type person struct {
	Name string
	Age  int
}

type personSlice []person

func (s personSlice) Len() int           { return len(s) }
func (s personSlice) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
func (s personSlice) Less(i, j int) bool { return s[i].Age &amp;lt; s[j].Age }

func main() {
	a := personSlice{
		{
			Name: &quot;AAA&quot;, 
			Age:  55, 
		}, 
		{
			Name: &quot;BBB&quot;, 
			Age:  22, 
		}, 
		{
			Name: &quot;CCC&quot;, 
			Age:  0, 
		}, 
		{
			Name: &quot;DDD&quot;, 
			Age:  22, 
		}, 
		{
			Name: &quot;EEE&quot;, 
			Age:  11, 
		}, 
	}
	sort.Sort(a)
	fmt.Println(&quot;Sort:&quot;, a)

	sort.Stable(a)
	fmt.Println(&quot;Stable:&quot;, a)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该示例程序的自定义类型personSlice实现了sort.Interface接口，所以可以将其对象作为sort.Sort()和sort.Stable()的参数传入。运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;程序输出：

Sort: [{CCC 0} {EEE 11} {BBB 22} {DDD 22} {AAA 55}]
Stable: [{CCC 0} {EEE 11} {BBB 22} {DDD 22} {AAA 55}]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;29.3 sort.Slice&lt;/h2&gt;
&lt;p&gt;利用sort.Slice 函数，而不用提供一个特定的 sort.Interface 的实现，而是 Less(i，j int) 作为一个比较回调函数，可以简单地传递给 sort.Slice 进行排序。这种方法一般不建议使用，因为在sort.Slice中使用了reflect。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;sort&quot;
)

type Peak struct {
	Name      string
	Elevation int // in feet
}

func main() {
	peaks := []Peak{
		{&quot;Aconcagua&quot;, 22838}, 
		{&quot;Denali&quot;, 20322}, 
		{&quot;Kilimanjaro&quot;, 19341}, 
		{&quot;Mount Elbrus&quot;, 18510}, 
		{&quot;Mount Everest&quot;, 29029}, 
		{&quot;Mount Kosciuszko&quot;, 7310}, 
		{&quot;Mount Vinson&quot;, 16050}, 
		{&quot;Puncak Jaya&quot;, 16024}, 
	}

	// does an in-place sort on the peaks slice, with tallest peak first
	sort.Slice(peaks, func(i, j int) bool {
		return peaks[i].Elevation &amp;gt;= peaks[j].Elevation
	})
	fmt.Println(peaks)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：
[{Mount Everest 29029} {Aconcagua 22838} {Denali 20322} {Kilimanjaro 19341} {Mount Elbrus 18510} {Mount Vinson 16050} {Puncak Jaya 16024} {Mount Kosciuszko 7310}]

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_28_unsafe.md&quot;&gt;第二十八章 unsafe包&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_30_os.md&quot;&gt;第三十章 OS包&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第三十章 os包</title><link>https://blog.wemang.com/posts/go/study/42_30_os/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_30_os/</guid><pubDate>Mon, 12 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;30.1 启动外部命令和程序&lt;/h2&gt;
&lt;p&gt;os标准包，是一个比较重要的包，顾名思义，主要是在服务器上进行系统的基本操作，如文件操作，目录操作，执行命令，信号与中断，进程，系统状态等等。在os包下，有
exec，signal，user三个子包。&lt;/p&gt;
&lt;p&gt;在os包中，有很多有趣的功能，比如可以通过变量Args来获取命令参数，os.Args返回一个字符串数组。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   fmt.Println(os.Args)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在os包中，相关函数名字和作用有较重的UNIX风格，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Chdir(dir string) error   //chdir将当前工作目录更改为dir目录
func Getwd() (dir string, err error)    //获取当前目录
func Chmod(name string, mode FileMode) error     //更改文件的权限
func Chown(name string, uid, gid int) error  //更改文件拥有者owner
func Chtimes(name string, atime time.Time, mtime time.Time) error 
func Clearenv()    //清除所有环境变量（慎用）
func Environ() []string  //返回所有环境变量
func Exit(code int)     //系统退出，并返回code，其中０表示执行成功并退出，非０表示错误并退出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在os包中，有关文件的处理也有很多方法，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Create(name string) (file *File, err error) // Create采用模式0666创建一个名为name的文件，如果文件已存在会截断它（为空文件）
func Open(name string) (file *File, err error) // Open打开一个文件用于读取。
func (f *File) Stat() (fi FileInfo, err error) // Stat返回描述文件f的FileInfo类型值
func (f *File) Readdir(n int) (fi []FileInfo, err error) // Readdir读取目录f的内容，返回一个有n个成员的[]FileInfo
func (f *File) Read(b []byte) (n int, err error) // Read方法从f中读取最多len(b)字节数据并写入b
func (f *File) WriteString(s string) (ret int, err error) // 向文件中写入字符串
func (f *File) Sync() (err error) // Sync递交文件的当前内容进行稳定的存储。
func (f *File) Close() error // Close关闭文件f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有关文件的处理，将在下一章中详细说明。&lt;/p&gt;
&lt;p&gt;在os 包中有一个 StartProcess 函数可以调用或启动外部系统命令和二进制可执行文件；它的第一个参数是要运行的进程，第二个参数用来传递选项或参数，第三个参数是含有系统环境基本信息的结构体。&lt;/p&gt;
&lt;p&gt;这个函数返回被启动进程的 id（pid），或者启动失败返回错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;os&quot;
)

func main() {
	// os.StartProcess 
	env := os.Environ()
	procAttr := &amp;amp;os.ProcAttr{
		Env: env, 
		Files: []*os.File{
			os.Stdin, 
			os.Stdout, 
			os.Stderr, 
		}, 
	}
	// example:
	Pid, err := os.StartProcess(&quot;/bin/ls&quot;, []string{&quot;ls&quot;, &quot;-l&quot;}, procAttr)
	if err != nil {
		fmt.Printf(&quot;Error %v starting process!&quot;, err) //
		os.Exit(1)
	}
	fmt.Printf(&quot;The process id is %v&quot;, Pid)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;30.2 os/signal 信号处理&lt;/h2&gt;
&lt;p&gt;一个运行良好的程序在退出（正常退出或者强制退出，如ctrl+c，kill等）时是可以执行一段清理代码，将收尾工作做完后再真正退出。一般采用系统Signal来通知系统退出，如kill pid。在程序中针对一些系统信号设置了处理函数，当收到信号后，会执行相关清理程序或通知各个子进程做自清理。&lt;/p&gt;
&lt;p&gt;Go的系统信号处理主要涉及os包、os.signal包以及syscall包。其中最主要的函数是signal包中的Notify函数：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;func Notify(c chan&amp;lt;- os.Signal, sig …os.Signal)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;该函数会将进程收到的系统Signal转发给channel c。如果没有传入sig参数，那么Notify会将系统收到的所有信号转发给channel c。&lt;/p&gt;
&lt;p&gt;Notify会根据传入的os.Signal，监听对应Signal信号，Notify()方法会将接收到对应os.Signal往一个channel c中发送。&lt;/p&gt;
&lt;p&gt;下面代码以 syscall.SIGUSR2 信息为例，说明了具体实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;os&quot;
	&quot;os/signal&quot;
	&quot;syscall&quot;
	&quot;time&quot;
)

func main() {
	go signalListen()
	for {
		time.Sleep(10 * time.Second)
	}
}

func signalListen() {
	c := make(chan os.Signal)
	signal.Notify(c, syscall.SIGUSR2)
	for {
		s := &amp;lt;-c
		//收到信号后的处理，这里只是输出信号内容，可以做一些更有意思的事
		fmt.Println(&quot;get signal:&quot;, s)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于信号有关信息，有兴趣建议可以参考《UNIX高级编程》。其他更多Signal信号类型，请参看相关手册。&lt;/p&gt;
&lt;p&gt;os包中其他的功能还有很多，这里就不一一介绍了。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_29_sort.md&quot;&gt;第二十九章 排序(sort)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_31_io.md&quot;&gt;第三十一章 文件操作与IO&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第二十八章 unsafe包</title><link>https://blog.wemang.com/posts/go/study/42_28_unsafe/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_28_unsafe/</guid><pubDate>Sat, 10 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;28.1 unsafe 包&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func Alignof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Sizeof(x ArbitraryType) uintptr
type ArbitraryType int
type Pointer *ArbitraryType
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在unsafe包中，只提供了3个函数，两个类型。就这么少的量，却有着超级强悍的功能。一般我们在C语言中通过指针，在知道变量在内存中占用的字节数情况下，就可以通过指针加偏移量的操作，直接在地址中，修改，访问变量的值。在Go 语言中不支持指针运算，那怎么办呢？其实通过unsafe包，我们可以完成类似的操作。&lt;/p&gt;
&lt;p&gt;ArbitraryType 是以int为基础定义的一个新类型，但是Go 语言unsafe包中，对ArbitraryType赋予了特殊的意义，通常，我们把interface{}看作是任意类型，那么ArbitraryType这个类型，在Go 语言系统中，比interface{}还要随意。&lt;/p&gt;
&lt;p&gt;Pointer 是ArbitraryType指针类型为基础的新类型，在Go 语言系统中，可以把Pointer类型，理解成任何指针的亲爹。&lt;/p&gt;
&lt;p&gt;Go 语言的指针类型长度与int类型长度，在内存中占用的字节数是一样的。ArbitraryType类型的变量也可以是指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Alignof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Sizeof(x ArbitraryType) uintptr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过分析发现，这三个函数的参数均是ArbitraryType类型。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Alignof返回变量对齐字节数量&lt;/li&gt;
&lt;li&gt;Offsetof返回变量指定属性的偏移量，所以如果变量是一个struct类型，不能直接将这个struct类型的变量当作参数，只能将这个struct类型变量的属性当作参数。&lt;/li&gt;
&lt;li&gt;Sizeof 返回变量在内存中占用的字节数，切记，如果是slice，则不会返回这个slice在内存中的实际占用长度。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;unsafe中，通过ArbitraryType 、Pointer 这两个类型，可以将其他类型都转换过来，然后通过这三个函数，分别能取长度，偏移量，对齐字节数，就可以在内存地址映射中，来回游走。&lt;/p&gt;
&lt;h2&gt;28.2 指针运算&lt;/h2&gt;
&lt;p&gt;uintptr这个基础类型，在Go 语言中，字节长度是与int一致。通常Pointer不能参与指针运算，比如你要在某个指针地址上加上一个偏移量，Pointer是不能做这个运算的，那么谁可以呢？这里要靠uintptr类型了，只有将Pointer类型先转换成uintptr类型，做完地址加减法运算后，再转换成Pointer类型，通过*操作达到取值、修改值的目的。&lt;/p&gt;
&lt;p&gt;unsafe.Pointer其实就是类似C的void *，在Go 语言中是用于各种指针相互转换的桥梁，也即是通用指针。它可以让任意类型的指针实现相互转换，也可以将任意类型的指针转换为 uintptr 进行指针运算。&lt;/p&gt;
&lt;p&gt;uintptr是Go 语言的内置类型，是能存储指针的整型， uintptr 的底层类型是int，它和unsafe.Pointer可相互转换。&lt;/p&gt;
&lt;p&gt;uintptr和unsafe.Pointer的区别就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;unsafe.Pointer只是单纯的通用指针类型，用于转换不同类型指针，它不可以参与指针运算；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;而uintptr是用于指针运算的，GC 不把 uintptr 当指针，也就是说 uintptr 无法持有对象， uintptr 类型的目标会被回收；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;unsafe.Pointer 可以和 普通指针 进行相互转换；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;unsafe.Pointer 可以和 uintptr 进行相互转换。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go 语言的unsafe包很强大，基本上很少会去用它。它可以像C一样去操作内存，但由于Go 语言不支持直接进行指针运算，所以用起来稍显麻烦。&lt;/p&gt;
&lt;p&gt;uintptr和intptr是无符号和有符号的指针类型，并且确保在64位平台上是8个字节，在32位平台上是4个字节，uintptr主要用于Go 语言中的指针运算。&lt;/p&gt;
&lt;p&gt;通过unsafe包来实现对V的成员i和j赋值，然后通过GetI()和GetJ()来打印观察输出结果。&lt;/p&gt;
&lt;p&gt;以下是main.go源代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;unsafe&quot;
)

type V struct {
	i int32
	j int64
}

func (v V) GetI() {
	fmt.Printf(&quot;i=%d\n&quot;, v.i)
}
func (v V) GetJ() {
	fmt.Printf(&quot;j=%d\n&quot;, v.j)
}

func main() {
	// 定义指针类型变量
	var v *V = &amp;amp;V{199, 299}

	// 取得v的指针并转为*int32的值，对应结构体的i。
	var i *int32 = (*int32)(unsafe.Pointer(v))

	fmt.Println(&quot;指针地址：&quot;, i)
	fmt.Println(&quot;指针uintptr值:&quot;, uintptr(unsafe.Pointer(i)))
	*i = int32(98)

	// 根据v的基准地址加上偏移量进行指针运算，运算后的值为j的地址，使用unsafe.Pointer转为指针
	var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int64(0)))))

	*j = int64(763)

	v.GetI()
	v.GetJ()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;指针地址： 0xc00000c180
指针uintptr值: 824633770368
i=98
j=763
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要修改struct字段的值，需要提前知道结构体V的成员布局，然后根据字段计算偏移量，以及考虑对齐值，最后通过指针运算得到成员指针，利用指针达到修改成员值得目的。由于结构体的成员在内存中的分配是一段连续的内存，因此结构体中第一个成员的地址就是这个结构体的地址，我们也可以认为是相对于这个结构体偏移了0。相同的，这个结构体中的任一成员都可以相对于这个结构体的偏移来计算出它在内存中的绝对地址。&lt;/p&gt;
&lt;p&gt;具体来讲解下main方法的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var v *V = &amp;amp;V{199, 299}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&amp;amp;来分配一段内存(并按类型初始化)，返回一个指针。所以v就是类型为V的一个指针。和new函数的作用类似。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var i *int32 = (*int32)(unsafe.Pointer(v))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将指针v转成通用指针，再转成int32指针类型。这里就看到了unsafe.Pointer的作用了，您不能直接将v转成int32类型的指针，那样将会panic，但是unsafe.Pointer是可以转为任何指针。刚才说了v的地址其实就是它的第一个成员的地址，所以这个i就很显然指向了v的成员i，通过给i赋值就相当于给v.i赋值了，但是别忘了i只是个指针，要赋值得解引用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*i = int32(98)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在已经成功的改变了v的私有成员i的值。&lt;/p&gt;
&lt;p&gt;但是对于v.j来说，怎么来得到它在内存中的地址呢？其实我们可以获取它相对于v的偏移量(unsafe.Sizeof可以为我们做这个事)，但上面的代码并没有这样去实现。各位别急，一步步来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int64(0)))))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实我们已经知道v是有两个成员的，包括i和j，并且在定义中，i位于j的前面，而i是int32类型，也就是说i占4个字节。所以j是相对于v偏移了4个字节。您可以用uintptr(4)或uintptr(unsafe.Sizeof(int64(0)))来做这个事。unsafe.Sizeof方法用来得到一个值应该占用多少个字节空间。注意这里跟C的用法不一样，C是直接传入类型，而Go 语言是传入值。&lt;/p&gt;
&lt;p&gt;之所以转成uintptr类型是因为需要做指针运算。v的地址加上j相对于v的偏移地址，也就得到了v.j在内存中的绝对地址，然后通过unsafe.Pointer转为指针，别忘了j的类型是int64，所以现在的j就是一个指向v.j的指针，接下来给它赋值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*j = int64(763)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，我们可以看到两种地址表示上的差异：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;指针地址： 0xc00000c180
指针uintptr值: 824633770368
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面结构体V中，定义了2个成员属性，如果我们定义一个byte类型的成员属性。我们来看下它的输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;unsafe&quot;
)

type V struct {
	b byte
	i int32
	j int64
}

func (v V) GetI() {
	fmt.Printf(&quot;i=%d\n&quot;, v.i)
}
func (v V) GetJ() {
	fmt.Printf(&quot;j=%d\n&quot;, v.j)
}

func main() {
	// 定义指针类型变量
	var v *V = new(V)

	// v的长度
	fmt.Printf(&quot;size=%d\n&quot;, unsafe.Sizeof(*v))
	// 取得v的指针考虑对齐值计算偏移量，然后转为*int32的值，对应结构体的i。
	var i *int32 = (*int32)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(4*unsafe.Sizeof(byte(0)))))

	fmt.Println(&quot;指针地址：&quot;, i)
	fmt.Println(&quot;指针uintptr值:&quot;, uintptr(unsafe.Pointer(i)))
	*i = int32(98)

	// 根据v的基准地址加上偏移量进行指针运算，运算后的值为j的地址，使用unsafe.Pointer转为指针
	var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int64(0)))))

	*j = int64(763)
	fmt.Println(&quot;指针uintptr值:&quot;, uintptr(unsafe.Pointer(&amp;amp;v.b)))
	fmt.Println(&quot;指针uintptr值:&quot;, uintptr(unsafe.Pointer(&amp;amp;v.i)))
	fmt.Println(&quot;指针uintptr值:&quot;, uintptr(unsafe.Pointer(&amp;amp;v.j)))
	v.GetI()
	v.GetJ()
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：
size=16
指针地址： 0xc000050084
指针uintptr值: 824634048644
指针uintptr值: 824634048640
指针uintptr值: 824634048644
指针uintptr值: 824634048648
i=98
j=763
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新结构体的长度为size=16，好像跟我们想像的不一致。我们计算一下：b是byte类型，占1个字节；i是int32类型，占4个字节；j是int64类型，占8个字节，1+4+8=13。这是怎么回事呢？&lt;/p&gt;
&lt;p&gt;这是因为发生了对齐。在struct中，它的对齐值是它的成员中的最大对齐值。&lt;/p&gt;
&lt;p&gt;每个成员类型都有它的对齐值，可以用unsafe.Alignof方法来计算，比如unsafe.Alignof(v.b)就可以得到b的对齐值为1 。但这个对齐值是其值类型的长度或引用的地址长度（32位或者64位），和其在结构体中的size不是简单相加的问题。经过在64位机器上测试，发现地址（uintptr）如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsafe.Pointer(b): %s 824634048640
unsafe.Pointer(i): %s 824634048644
unsafe.Pointer(j): %s 824634048648
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以初步推断，也经过测试验证，取i值使用uintptr(4*unsafe.Sizeof(byte(0)))是准确的。至于size其实也和对齐值有关，也不是简单相加每个字段的长度。&lt;/p&gt;
&lt;p&gt;unsafe.Offsetof 可以在实际中使用，如果改变私有的字段，需要程序员认真考虑后，按照上面的方法仔细确认好对齐值再进行操作。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_27_reflect.md&quot;&gt;第二十七章 反射(reflect)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_29_sort.md&quot;&gt;第二十九章 排序(sort)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第二十七章 反射(reflect)</title><link>https://blog.wemang.com/posts/go/study/42_27_reflect/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_27_reflect/</guid><pubDate>Fri, 09 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;27.1 反射(reflect)&lt;/h2&gt;
&lt;p&gt;反射是应用程序检查其所拥有的结构，尤其是类型的一种能。每种语言的反射模型都不同，并且有些语言根本不支持反射。Go语言实现了反射，反射机制就是在运行时动态调用对象的方法和属性，即可从运行时态的示例对象反求其编码阶段的定义，标准库中reflect包提供了相关的功能。在reflect包中，通过reflect.TypeOf()，reflect.ValueOf()分别从类型、值的角度来描述一个Go对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TypeOf(i interface{}) Type
type Type interface 

func ValueOf(i interface{}) Value
type Value struct 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Go语言的实现中，一个interface类型的变量存储了2个信息, 一个&lt;code&gt;&amp;lt;值，类型&amp;gt;&lt;/code&gt;对，&lt;code&gt;&amp;lt;value,type&amp;gt;&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(value, type)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;value是实际变量值，type是实际变量的类型。两个简单的函数，reflect.TypeOf 和 reflect.ValueOf，返回被检查对象的类型和值。&lt;/p&gt;
&lt;p&gt;例如，x 被定义为：var x float64 = 3.4，那么 reflect.TypeOf(x) 返回 float64，reflect.ValueOf(x) 返回 3.4。实际上，反射是通过检查一个接口的值，变量首先被转换成空接口。这从下面两个函数签名能够很明显的看出来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;reflect.Type 和 reflect.Value 都有许多方法用于检查和操作它们。&lt;/p&gt;
&lt;p&gt;Type主要有：
Kind() 将返回一个常量，表示具体类型的底层类型
Elem()方法返回指针、数组、切片、字典、通道的基类型，这个方法要慎用，如果用在其他类型上面会出现panic&lt;/p&gt;
&lt;p&gt;Value主要有：
Type() 将返回具体类型所对应的 reflect.Type（静态类型）
Kind() 将返回一个常量，表示具体类型的底层类型&lt;/p&gt;
&lt;p&gt;反射可以在运行时检查类型和变量，例如它的大小、方法和 动态 的调用这些方法。这对于没有源代码的包尤其有用。&lt;/p&gt;
&lt;p&gt;由于反射是一个强大的工具，但反射对性能有一定的影响，除非有必要，否则应当避免使用或小心使用。下面代码针对int、数组以及结构体分别使用反射机制，其中的差异请看注释。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;reflect&quot;
)

type Student struct {
	name string
}

func main() {

	var a int = 50
	v := reflect.ValueOf(a) // 返回Value类型对象，值为50
	t := reflect.TypeOf(a)  // 返回Type类型对象，值为int
	fmt.Println(v, t, v.Type(), t.Kind())

	var b [5]int = [5]int{5, 6, 7, 8}
	fmt.Println(reflect.TypeOf(b), reflect.TypeOf(b).Kind(),reflect.TypeOf(b).Elem()) // [5]int array int

	var Pupil Student
	p := reflect.ValueOf(Pupil) // 使用ValueOf()获取到结构体的Value对象

	fmt.Println(p.Type()) // 输出:Student
	fmt.Println(p.Kind()) // 输出:struct

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Go语言中，类型包括 static type和concrete type. 简单说 static type是你在编码是看见的类型(如int、string)，concrete type是实际具体的类型，runtime系统看见的类型。&lt;/p&gt;
&lt;p&gt;Type()返回的是静态类型，而kind()返回的是具体类型。上面代码中，在int，数组以及结构体三种类型情况中，可以看到kind()，type()返回值的差异。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通过反射可以修改原对象&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;d.CanAddr()方法：判断它是否可被取地址
d.CanSet()方法：判断它是否可被取地址并可被修改&lt;/p&gt;
&lt;p&gt;通过一个settable的Value反射对象来访问、修改其对应的变量值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;reflect&quot;
)

type Student struct {
	name string
	Age  int
}

func main() {

	var a int = 50
	v := reflect.ValueOf(a) // 返回Value类型对象，值为50
	t := reflect.TypeOf(a)  // 返回Type类型对象，值为int
	fmt.Println(v, t, v.Type(), t.Kind(), reflect.ValueOf(&amp;amp;a).Elem())
	seta := reflect.ValueOf(&amp;amp;a).Elem() // 这样才能让seta保存a的值
	fmt.Println(seta, seta.CanSet())
	seta.SetInt(1000)
	fmt.Println(seta)

	var b [5]int = [5]int{5, 6, 7, 8}
	fmt.Println(reflect.TypeOf(b), reflect.TypeOf(b).Kind(), reflect.TypeOf(b).Elem())

	var Pupil Student = Student{&quot;joke&quot;, 18}
	p := reflect.ValueOf(Pupil) // 使用ValueOf()获取到结构体的Value对象

	fmt.Println(p.Type()) // 输出:Student
	fmt.Println(p.Kind()) // 输出:struct

	setStudent := reflect.ValueOf(&amp;amp;Pupil).Elem()
	//setStudent.Field(0).SetString(&quot;Mike&quot;) // 未导出字段，不能修改，panic会发生
	setStudent.Field(1).SetInt(19)
	fmt.Println(setStudent)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然反射可以越过Go语言的导出规则的限制读取结构体中未导出的成员，但不能修改这些未导出的成员。因为一个结构体中只有被导出的字段才是可修改的。&lt;/p&gt;
&lt;p&gt;在结构体中有tag标签，通过反射可获取结构体成员变量的tag信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;reflect&quot;
)

type Student struct {
	name string
	Age  int `json:&quot;years&quot;`
}

func main() {
	var Pupil Student = Student{&quot;joke&quot;, 18}
	setStudent := reflect.ValueOf(&amp;amp;Pupil).Elem()

	sSAge, _ := setStudent.Type().FieldByName(&quot;Age&quot;)
	fmt.Println(sSAge.Tag.Get(&quot;json&quot;)) // years
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：
years
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;27.2 反射结构体&lt;/h2&gt;
&lt;p&gt;为了完整说明反射的情况，通过反射一个结构体类型，综合来说明。下面例子较为系统地利用一个结构体，来充分举例说明反射：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;reflect&quot;
)

// 结构体
type ss struct {
	int
	string
	bool
	float64
}

func (s ss) Method1(i int) string  { return &quot;结构体方法1&quot; }
func (s *ss) Method2(i int) string { return &quot;结构体方法2&quot; }

var (
	structValue = ss{ // 结构体
		20, 
		&quot;结构体&quot;, 
		false, 
		64.0, 
	}
)

// 复杂类型
var complexTypes = []interface{}{
	structValue, &amp;amp;structValue, // 结构体
	structValue.Method1, structValue.Method2, // 方法
}

func main() {
	// 测试复杂类型
	for i := 0; i &amp;lt; len(complexTypes); i++ {
		PrintInfo(complexTypes[i])
	}
}

func PrintInfo(i interface{}) {
	if i == nil {
		fmt.Println(&quot;--------------------&quot;)
		fmt.Printf(&quot;无效接口值：%v\n&quot;, i)
		fmt.Println(&quot;--------------------&quot;)
		return
	}
	v := reflect.ValueOf(i)
	PrintValue(v)
}

func PrintValue(v reflect.Value) {
	fmt.Println(&quot;--------------------&quot;)
	// ----- 通用方法 -----
	fmt.Println(&quot;String             :&quot;, v.String())  // 反射值的字符串形式
	fmt.Println(&quot;Type               :&quot;, v.Type())    // 反射值的类型
	fmt.Println(&quot;Kind               :&quot;, v.Kind())    // 反射值的类别
	fmt.Println(&quot;CanAddr            :&quot;, v.CanAddr()) // 是否可以获取地址
	fmt.Println(&quot;CanSet             :&quot;, v.CanSet())  // 是否可以修改
	if v.CanAddr() {
		fmt.Println(&quot;Addr               :&quot;, v.Addr())       // 获取地址
		fmt.Println(&quot;UnsafeAddr         :&quot;, v.UnsafeAddr()) // 获取自由地址
	}
	// 获取方法数量
	fmt.Println(&quot;NumMethod          :&quot;, v.NumMethod())
	if v.NumMethod() &amp;gt; 0 {
		// 遍历方法
		i := 0
		for ; i &amp;lt; v.NumMethod()-1; i++ {
			fmt.Printf(&quot;    ┣ %v\n&quot;, v.Method(i).String())
			//			if i &amp;gt;= 4 { // 只列举 5 个
			//				fmt.Println(&quot;    ┗ ...&quot;)
			//				break
			//			}
		}
		fmt.Printf(&quot;    ┗ %v\n&quot;, v.Method(i).String())
		// 通过名称获取方法
		fmt.Println(&quot;MethodByName       :&quot;, v.MethodByName(&quot;String&quot;).String())
	}

	switch v.Kind() {
	// 结构体：
	case reflect.Struct:
		fmt.Println(&quot;=== 结构体 ===&quot;)
		// 获取字段个数
		fmt.Println(&quot;NumField           :&quot;, v.NumField())
		if v.NumField() &amp;gt; 0 {
			var i int
			// 遍历结构体字段
			for i = 0; i &amp;lt; v.NumField()-1; i++ {
				field := v.Field(i) // 获取结构体字段
				fmt.Printf(&quot;    ├ %-8v %v\n&quot;, field.Type(), field.String())
			}
			field := v.Field(i) // 获取结构体字段
			fmt.Printf(&quot;    └ %-8v %v\n&quot;, field.Type(), field.String())
			// 通过名称查找字段
			if v := v.FieldByName(&quot;ptr&quot;); v.IsValid() {
				fmt.Println(&quot;FieldByName(ptr)   :&quot;, v.Type().Name())
			}
			// 通过函数查找字段
			v := v.FieldByNameFunc(func(s string) bool { return len(s) &amp;gt; 3 })
			if v.IsValid() {
				fmt.Println(&quot;FieldByNameFunc    :&quot;, v.Type().Name())
			}
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：
String             : &amp;lt;main.ss Value&amp;gt;
Type               : main.ss
Kind               : struct
CanAddr            : false
CanSet             : false
NumMethod          : 1
    ┗ &amp;lt;func(int) string Value&amp;gt;
MethodByName       : &amp;lt;invalid Value&amp;gt;
=== 结构体 ===
NumField           : 4
    ├ int      &amp;lt;int Value&amp;gt;
    ├ string   结构体
    ├ bool     &amp;lt;bool Value&amp;gt;
    └ float64  &amp;lt;float64 Value&amp;gt;
--------------------
String             : &amp;lt;*main.ss Value&amp;gt;
Type               : *main.ss
Kind               : ptr
CanAddr            : false
CanSet             : false
NumMethod          : 2
    ┣ &amp;lt;func(int) string Value&amp;gt;
    ┗ &amp;lt;func(int) string Value&amp;gt;
MethodByName       : &amp;lt;invalid Value&amp;gt;
--------------------
String             : &amp;lt;func(int) string Value&amp;gt;
Type               : func(int) string
Kind               : func
CanAddr            : false
CanSet             : false
NumMethod          : 0
--------------------
String             : &amp;lt;func(int) string Value&amp;gt;
Type               : func(int) string
Kind               : func
CanAddr            : false
CanSet             : false
NumMethod          : 0

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;细心的读者可能发现了上面代码中的一个有趣的问题，那就是structValue, &amp;amp;structValue的反射结果是不一样的，指针对象在这里有两个方法，而值对象只有一个方法，这是因为Method2()方法是指针方法，在值对象中是不能被反射到的。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_26_testing.md&quot;&gt;第二十六章 测试&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_28_unsafe.md&quot;&gt;第二十八章 unsafe包&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第二十六章 测试</title><link>https://blog.wemang.com/posts/go/study/42_26_testing/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_26_testing/</guid><pubDate>Thu, 08 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在Go语言中，所有的包都应该有必要文档和注释，当然同样甚至更为重要的是对包进行必要的测试。&lt;/p&gt;
&lt;p&gt;testing 包就是这样一个标准包，被专门用来进行单元测试以及进行自动化测试，打印日志和错误报告，方便程序员调试代码，并且还包含一些基准测试函数的功能。&lt;/p&gt;
&lt;p&gt;testing 包含测试函数、测试辅助代码和示例函数；测试函数包括Test开头的单元测试函数和以Benchmark开头的基准测试函数两种，测试辅助代码是为测试函数服务的公共函数、初始化函数、测试数据等。&lt;/p&gt;
&lt;p&gt;而示例函数则是名称以Example开头函数，通常保存在example_*_test.go文件中，示例函数检测实际输出与注释中的期望输出是否一致，一致则测试通过，不一致则测试失败。&lt;/p&gt;
&lt;h2&gt;26.1 单元测试&lt;/h2&gt;
&lt;p&gt;开发中经常需要对一个包做（单元）测试，写一些可以频繁（每次更新后）执行的小块测试单元来检查代码的正确性，于是我们必须写一些 Go 源文件来测试代码。&lt;/p&gt;
&lt;p&gt;使用testing包，我们只需要遵守简单的规则，就可以很好地写出通用的测试程序。因为其他开发人员也会遵循这个包的规则来进行测试。&lt;/p&gt;
&lt;p&gt;首先测试程序是独立的文件，他必须属于被测试的包，和这个包的其他程序放在一起，并且文件名满足这种形式 *_test.go。由于是独立的测试文件，所以测试代码和包中的业务代码是分开的。Go语言这样规定的好处是不言而喻的，因为在其他语言开发的程序中，我们经常可以看到代码中注释掉的测试代码，而且有把开发版作为生产版发布到线上导致异常的问题出现。&lt;/p&gt;
&lt;p&gt;当然，好的规则需要我们遵守并严格执行。&lt;/p&gt;
&lt;p&gt;_test 程序不会被普通的 Go 编译器编译，所以当放应用部署到生产环境时它们不会被部署；只有 Gotest 会编译所有的程序：普通程序和测试程序。&lt;/p&gt;
&lt;p&gt;测试文件中必须导入 &quot;testing&quot; 包，测试函数名字是以 TestXxx 打头的全局函数，Xxx部分可以为任意的字母数字的组合，但是首字母不能是小写字母[a-z]，函数名我们可以以被测试函数的字母描述，如 TestFmtInterface，TestPayEmployees 等。测试用例会按照测试源代码中写的顺序依次执行。&lt;/p&gt;
&lt;p&gt;测试函数一般都要求这种形式的头部：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func TestAbcde(t *testing.T)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;*testing.T是传给测试函数的结构类型，用来管理测试状态，支持格式化测试日志，如 t.Log，t.Error，t.ErrorF 等。t.Log函数就像我们常常使用的fmt.Println一样，可以接受多个参数，方便输出调试结果。&lt;/p&gt;
&lt;p&gt;用下面这些函数来通知测试失败：
1）func (t *T) Fail()
标记测试函数为失败，然后继续执行剩下的测试。&lt;/p&gt;
&lt;p&gt;2）func (t *T) FailNow()
标记测试函数为失败并中止执行；文件中别的测试也被略过，继续执行下一个文件。&lt;/p&gt;
&lt;p&gt;3）func (t *T) Log(args ...interface{})
args 被用默认的格式格式化并打印到错误日志中。&lt;/p&gt;
&lt;p&gt;4）func (t *T) Fatal(args ...interface{})
结合 先执行 3），然后执行 2）的效果。&lt;/p&gt;
&lt;p&gt;运行 go test 来编译测试程序，并执行程序中所有的 TestXxx 函数。如果所有的测试都通过会打印出 PASS。&lt;/p&gt;
&lt;p&gt;当然，对于包中不能导出的函数不能进行单元或者基准测试。&lt;/p&gt;
&lt;p&gt;gotest 可以接收一个或多个函数程序作为参数，并指定一些选项。&lt;/p&gt;
&lt;p&gt;go test 常用参数
-cpu: 指定测试的GOMAXPROCS值，默认是GOMAXPROCS当前值
-count: 运行单元测试和基准测试n次（默认1）。如设置了-cpu，则为每个GOMAXPROCS运行n次，示例函数总运行一次。
-cover: 启用覆盖率分析
-run: 执行功能测试函数，支持正则匹配，可以选择测试函数或者测试文件来仅测试单个函数或者单个文件
-bench: 执行基准测试函数，支持正则匹配
-benchtime: 基准测试最大时间上限
-parallel: 允许并行执行的最大测试数，默认情况下设置为GOMAXPROCS的值
-v: 展示测试过程信息&lt;/p&gt;
&lt;p&gt;在系统标准包中，有很多 _test.go 结尾的程序，大家可以用来测试，为节约篇幅这里我就不写具体例子了。&lt;/p&gt;
&lt;h2&gt;26.2 基准测试&lt;/h2&gt;
&lt;p&gt;testing 包中有一些类型和函数可以用来做简单的基准测试；测试代码中必须包含以 BenchmarkZzz 打头的函数并接收一个 *testing.B 类型的参数，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func BenchmarkReverse(b *testing.B) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令 go test –test.bench=.* 会运行所有的基准测试函数；代码中的函数会被调用 N 次（N是非常大的数，如 N = 1000000），可以根据情况指定b.N的值，并展示 N 的值和函数执行的平均时间，单位为 ns（纳秒，ns/op）。如果是用 testing.Benchmark 调用这些函数，直接运行程序即可。&lt;/p&gt;
&lt;p&gt;下面我们看一个测试的具体例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package even

func Loop(n uint64) (result uint64) {
	result = 1
	var i uint64 = 1
	for ; i &amp;lt;= n; i++ {
		result *= i
	}
	return result
}

func Factorial(n uint64) (result uint64) {
	if n &amp;gt; 0 {
		result = n * Factorial(n-1)
		return result
	}
	return 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 even 包的路径下，我们创建一个名为 even_test.go 的测试程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package even

import (
	&quot;testing&quot;
)

func TestLoop(t *testing.T) {
	t.Log(&quot;Loop:&quot;, Loop(uint64(32)))
}

func TestFactorial(t *testing.T) {
	t.Log(&quot;Factorial:&quot;, Factorial(uint64(32)))
}

func BenchmarkLoop(b *testing.B) {

	for i := 0; i &amp;lt; b.N; i++ {
		Loop(uint64(40))
	}
}

func BenchmarkFactorial(b *testing.B) {

	for i := 0; i &amp;lt; b.N; i++ {
		Factorial(uint64(40))
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们可以在这个包的目录下使用命令：go test  -test.bench=.* 来测试 even 包。&lt;/p&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输出：

goos: windows
goarch: amd64
pkg: go42/chapter-13/13.1/1
BenchmarkLoop-4        	50000000	        27.2 ns/op
BenchmarkFactorial-4   	10000000	       163 ns/op
PASS
ok  	go42/chapter-13/13.1/1	3.628s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;递归函数的确是很耗费系统资源，而且运行也慢，不建议使用。&lt;/p&gt;
&lt;h2&gt;26.3 分析并优化 Go 程序&lt;/h2&gt;
&lt;p&gt;如果代码使用了 Go 中 testing 包的基准测试功能，我们可以用 gotest 标准的 -cpuprofile 和 -memprofile 标志向指定文件写入 CPU 或 内存使用情况报告。&lt;/p&gt;
&lt;p&gt;使用方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go test -x -v -test.cpuprofile=pprof.out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行上面代码，将会基于基准测试把执行结果中的 cpu 性能分析信息写到 pprof.out 文件中。我们可以根据这个文件做分析来详细了解性能情况。&lt;/p&gt;
&lt;h2&gt;26.4 用 pprof 调试&lt;/h2&gt;
&lt;p&gt;要监控Go程序的堆栈，cpu的耗时等性能信息，我们可以通过使用pprof包来实现。在代码中，pprof包有两种方式导入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;net/http/pprof&quot;
&quot;runtime/prof&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实net/http/pprof中只是使用runtime/pprof包来进行封装了一下，并在http端口上暴露出来，让我们可以在浏览器查看程序的性能分析。我们可以自行查看net/http/pprof中代码，只有一个文件pprof.go。&lt;/p&gt;
&lt;p&gt;下面我们具体说说怎么使用pprof，首先我们讲讲在开发中取得pprof信息的三种方式：&lt;/p&gt;
&lt;p&gt;一：web 服务器程序&lt;/p&gt;
&lt;p&gt;如果我们的Go程序是web服务器，你想查看自己的web服务器的状态。这个时候就可以选择net/http/pprof。你只需要引入包_&quot;net/http/pprof&quot;，然后就可以在浏览器中使用&lt;code&gt;http://localhost:port/debug/pprof/&lt;/code&gt;直接看到当前web服务的状态，包括CPU占用情况和内存使用情况等。&lt;/p&gt;
&lt;p&gt;这里port是8080，也就是我们web服务器监听的端口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;net/http&quot;
	_ &quot;net/http/pprof&quot;  // 为什么用_ , 在讲解http包时有解释。
)

func myfunc(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, &quot;hi&quot;)
}

func main() {
	http.HandleFunc(&quot;/&quot;, myfunc)
	http.ListenAndServe(&quot;:8080&quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问&lt;code&gt;http://localhost:8080/debug/pprof/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;二：服务进程&lt;/p&gt;
&lt;p&gt;如果你的Go程序不是web服务器，而是一个服务进程，可以选择使用net/http/pprof包，然后开启一个goroutine来监听相应端口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	_ &quot;net/http/pprof&quot;

	&quot;time&quot;
)

func main() {
	// 开启pprof
	go func() {
		log.Println(http.ListenAndServe(&quot;localhost:8080&quot;, nil))
	}()
	go hello()
	select {}
}
func hello() {
	for {
		go func() {
			fmt.Println(&quot;hello word&quot;)
		}()
		time.Sleep(time.Millisecond * 1)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问&lt;code&gt;http://localhost:8080/debug/pprof/&lt;/code&gt;
在前面这两种方式中，我们还可以在命令行分别运行以下命令：&lt;/p&gt;
&lt;p&gt;利用这个命令查看堆栈信息：
&lt;code&gt;go tool pprof http://localhost:8080/debug/pprof/heap&lt;/code&gt;
利用这个命令可以查看程序CPU使用情况信息：
&lt;code&gt;go tool pprof http://localhost:8080/debug/pprof/profile&lt;/code&gt;
使用这个命令可以查看block信息：
&lt;code&gt;go tool pprof http://localhost:8080/debug/pprof/block&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/gotool.png&quot; alt=&quot;gotool.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里需要先安装graphviz，http://www.graphviz.org/download/ ，windows平台直接下载zip包，解压缩后把bin目录放到$path中。我们可以通过执行命令 png 产生图片，还有svg，gif，pdf等命令，生成的图片自动命名存放在当前目录下，我们这里生成了png。其他命令使用可通过help查看。&lt;/p&gt;
&lt;p&gt;三：应用程序&lt;/p&gt;
&lt;p&gt;如果你的Go程序只是一个应用程序，那么你就不能使用net/http/pprof包了，你就需要使用到runtime/pprof。比如下面的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;flag&quot;
	&quot;fmt&quot;
	&quot;log&quot;

	&quot;os&quot;
	&quot;runtime/pprof&quot;
	&quot;time&quot;
)

var cpuprofile = flag.String(&quot;cpuprofile&quot;, &quot;&quot;, &quot;write cpu profile to file&quot;)

func Factorial(n uint64) (result uint64) {
	if n &amp;gt; 0 {
		result = n * Factorial(n-1)
		return result
	}
	return 1
}

func main() {
	flag.Parse()
	if *cpuprofile != &quot;&quot; {
		f, err := os.Create(*cpuprofile)
		if err != nil {
			log.Fatal(err)
		}
		pprof.StartCPUProfile(f)
		defer pprof.StopCPUProfile()
	}

	go compute()
	time.Sleep(10 * time.Second)
}
func compute() {
	for i := 0; i &amp;lt; 100; i++ {
		go func() {
			fmt.Println(Factorial(uint64(40)))
		}()
		time.Sleep(time.Millisecond * 1)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译后生成3.exe文件并运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3.exe --cpuprofile=cpu.prof
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们编译后可执行程序是3.exe , 程序运行完后的cpu信息就会记录到cpu.prof中。&lt;/p&gt;
&lt;p&gt;现在有了cpu.prof 文件，我们就可以通过go tool pprof 来看相应的信息了。在命令行运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go tool pprof 3.exe cpu.prof 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里要注意的是需要带上可执行的程序名以及prof信息文件。&lt;/p&gt;
&lt;p&gt;命令执行后会进入到：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/132.png&quot; alt=&quot;132.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;命令界面和前面两种使用net/http/pprof包 一样。我们可以通过go tool pprof 生svg，png或者是pdf文件。&lt;/p&gt;
&lt;p&gt;这是生成的png文件，和前面生成的png类似，前面我们生成的是block信息：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/profile001.png&quot; alt=&quot;profile001.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过上面这三种情况的分析，我们可以知道，其实就是两种情况：
go tool pprof http://localhost:8080/debug/pprof/profile 这种url方式，或者
go tool pprof 3.exe cpu.prof   这种文件方式来进行分析。&lt;/p&gt;
&lt;p&gt;我们可以根据项目情况灵活使用。有关pprof，我们就讲这么多，在实际项目中，我们多使用就会发现这个工具还是蛮有用处的。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_25_oo.md&quot;&gt;第二十五章 面向对象&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_27_reflect.md&quot;&gt;第二十七章 反射(reflect)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第二十五章 面向对象</title><link>https://blog.wemang.com/posts/go/study/42_25_oo/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_25_oo/</guid><pubDate>Wed, 07 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;25.1 Go 中的面向对象&lt;/h2&gt;
&lt;p&gt;我们总结一下前面看到的：Go 没有类，而是松耦合的类型、方法对接口的实现。&lt;/p&gt;
&lt;p&gt;OO 语言最重要的三个方面分别是：封装，继承和多态，在 Go 中它们是怎样表现的呢？&lt;/p&gt;
&lt;p&gt;Go实现面向对象的两个关键是struct和interface，结构代替类，因为Go语言不提供类，但提供了结构体或自定义类型，方法可以被添加到结构体或自定义类型中。结构体之间可以嵌套，类似继承。而interface定义接口，实现多态性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;封装（数据隐藏）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;和别的 OO 语言有 4 个或更多的访问层次相比，Go 把它简化为了 2 层：
1）包范围内的：通过标识符首字母小写，对象 只在它所在的包内可见
2）可导出的：通过标识符首字母大写，对象 对所在包以外也可见类型只拥有自己所在包中定义的方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;继承：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Go没有显式的继承，而是通过组合实现继承，内嵌一个（或多个）包含想要的行为（字段和方法）的结构体；多重继承可以通过内嵌多个结构体实现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多态：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;多态是运行时特性，而继承则是编译时特征，也就是说，继承关系在编译时就已经确定了，而多态则可以实现运行时的动态绑定。Go用接口实现多态，某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的，并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 和 C# 接口的变体，而且：接口间是不相关的，并且是大规模编程和可适应的演进型设计的关键。&lt;/p&gt;
&lt;p&gt;另外Go没有构造函数，如果一定要在初始化对象的时候进行一些工作的话，可以自行封装产生实例的方法。实例化的时候可以初始化属性值，如果没有指明则默认为系统默认值。加&amp;amp;符号和new的是指针对象，没有的则是值对象，在传递对象的时候要根据实际情况来决定是要传递指针还是值。&lt;/p&gt;
&lt;h2&gt;25.2 多重继承&lt;/h2&gt;
&lt;p&gt;多重继承指的是类型获得多个父类型行为的能力，它在传统的面向对象语言中通常是不被实现的（C++ 和 Python 例外）。因为在类继承层次中，多重继承会给编译器引入额外的复杂度。但是在 Go 语言中，通过在类型中嵌入所有必要的父类型，可以很简单的实现多重继承。&lt;/p&gt;
&lt;p&gt;有关方法重载就是一个类中可以有相同的函数名称，但是它们的参数是不一致的，在java、C++中这种做法普遍存在。Go中如果尝试这么做会报重新声明（redeclared）错误，但是Go的函数可以声明不定参数，这个非常强大。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Println(a ...interface{}) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中a ...interface{}表示参数不定的意思。如果要根据不同的参数实现不同的功能，要在方法内检测传递的参数。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_24_pointer.md&quot;&gt;第二十四章 指针和内存&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_26_testing.md&quot;&gt;第二十六章 测试&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第二十四章 指针和内存</title><link>https://blog.wemang.com/posts/go/study/42_24_pointer/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_24_pointer/</guid><pubDate>Tue, 06 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;24.1 指针&lt;/h2&gt;
&lt;p&gt;一个指针变量可以指向任何一个值的内存地址。指针变量在 32 位计算机上占用 4B 内存，在 64 位计算机占用 8B内存，并且与它所指向的值的大小无关，因为指针变量只是地址的值而已。可以声明指针指向任何类型的值来表明它的原始性或结构性，也可以在指针类型前面加上*号（前缀）来获取指针所指向的内容。&lt;/p&gt;
&lt;p&gt;在Go语言中，指针类型表示指向给定类型（称为指针的基础类型）的变量的所有指针的集合。 符号 * 可以放在一个类型前，如 *T，那么它将以类型T为基础，生成指针类型*T。未初始化指针的值为nil。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Point3D struct{ x, y, z float64 }
var pointer *Point3D
var i *[4]int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面定义了两个指针类型变量。它们的值为nil，这时对它们的反向引用是不合法的，并且会使程序崩溃。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xx := (*pointer).x
panic: runtime error: invalid memory address or nil pointer dereference
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;符号 * 可以放在一个指针前，如 (*pointer)，那么它将得到这个指针指向地址上所存储的值，这称为反向引用。不过在Go语言中，(*pointer).x可以简写为pointer.x。&lt;/p&gt;
&lt;p&gt;对于任何一个变量 var， 表达式var == *(&amp;amp;var)都是正确的。&lt;/p&gt;
&lt;p&gt;注意：不能得到一个数字或常量的地址，下面的写法是错误的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const i = 5
ptr := &amp;amp;i // error: cannot take the address of i
ptr2 := &amp;amp;10 // error: cannot take the address of 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然Go 语言和 C、C++ 这些语言一样，都有指针的概念，但是指针运算在语法上是不允许的。这样做的目的是保证内存安全。从这一点看，Go 语言的指针基本就是一种引用。&lt;/p&gt;
&lt;p&gt;指针的一个高级应用是可以传递一个变量的引用（如函数的参数），这样不会传递变量的副本。当调用函数时，如果参数为基础类型，传进去的是值，也就是另外复制了一份参数到当前的函数调用栈。参数为引用类型时，传进去的基本都是引用。而指针传递的成本很低，只占用 4B或 8B内存。&lt;/p&gt;
&lt;p&gt;如果代码在运行中需要占用大量的内存，或很多变量，或者两者都有，这时使用指针会减少内存占用和提高运行效率。被指向的变量保存在内存中，直到没有任何指针指向它们。所以从它们被创建开始就具有相互独立的生命周期。&lt;/p&gt;
&lt;p&gt;内存管理中的内存区域一般包括堆内存（heap）和栈内存（stack）， 栈内存主要用来存储当前调用栈用到的简单类型数据，如string，bool，int，float 等。这些类型基本上较少占用内存，容易回收，因此可以直接复制，进行垃圾回收时也比较容易做针对性的优化。 而复杂的复合类型占用的内存往往相对较大，存储在堆内存中，垃圾回收频率相对较低，代价也较大，因此传引用或指针可以避免进行成本较高的复制操作，并且节省内存，提高程序运行效率。&lt;/p&gt;
&lt;p&gt;因此，在需要改变参数的值或者避免复制大批量数据而节省内存时（也会提高运行效率，毕竟大批量复制也耗费时间）都会选择使用指针。&lt;/p&gt;
&lt;p&gt;另一方面，指针的频繁使用也会导致性能下降。指针也可以指向另一个指针，并且可以进行任意深度的嵌套，形成多级的间接引用，但会使代码结构不清晰。&lt;/p&gt;
&lt;p&gt;在大多数情况下，Go 语言可以使程序员轻松创建指针，并且隐藏间接引用，如：自动反向引用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;指针的使用方法：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定义指针变量；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;为指针变量赋值；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;访问指针变量中指向地址的值；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在指针类型前面加上*号来获取指针所指向的内容。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
	var a, b int = 20, 30 // 声明实际变量
	var ptra *int         // 声明指针变量
	var ptrb *int = &amp;amp;b

	ptra = &amp;amp;a // 指针变量的存储地址

	fmt.Printf(&quot;a  变量的地址是: %x\n&quot;, &amp;amp;a)
	fmt.Printf(&quot;b  变量的地址是: %x\n&quot;, &amp;amp;b)

	// 指针变量的存储地址
	fmt.Printf(&quot;ptra  变量的存储地址: %x\n&quot;, ptra)
	fmt.Printf(&quot;ptrb  变量的存储地址: %x\n&quot;, ptrb)

	// 使用指针访问值
	fmt.Printf(&quot;*ptra  变量的值: %d\n&quot;, *ptra)
	fmt.Printf(&quot;*ptrb  变量的值: %d\n&quot;, *ptrb)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;24.2 new() 和 make() 的区别&lt;/h2&gt;
&lt;p&gt;new() 和 make() 都在堆上分配内存，但是它们的行为不同，适用于不同的类型。&lt;/p&gt;
&lt;p&gt;new() 用于值类型的内存分配，并且置为零值。
make() 只用于切片、字典以及通道这三种引用数据类型的内存分配和初始化。&lt;/p&gt;
&lt;p&gt;new(T) 分配类型 T 的零值并返回其地址，也就是指向类型 T 的指针。
make(T) 返回类型T的值（不是* T）。&lt;/p&gt;
&lt;p&gt;然而在Go语言中，并不能准确判断变量是分配到栈还是堆上。在C++中，使用new()创建的变量总是在堆上。在Go中变量的位置是由编译器决定的。编译器根据变量的大小和泄露（逃逸）分析的结果来决定其位置。&lt;/p&gt;
&lt;p&gt;如果想确切知道变量分配的位置，可在执行go build或go run时加上-m gc标志（即go run -gcflags -m app.go）。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go run -gcflags -m main.go
# command-line-arguments
.\main.go:12:31: m.Alloc / 1024 escapes to heap
.\main.go:11:23: main &amp;amp;m does not escape
.\main.go:12:12: main ... argument does not escape
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;24.3 垃圾回收和 SetFinalizer&lt;/h2&gt;
&lt;p&gt;Go 语言开发者一般不需要写代码来释放不再使用的变量或结构体占用的内存，在 Go语言运行时有一个独立的进程，即垃圾收集器（GC），会专门处理这些事情，它搜索不再使用的变量然后释放它们占用的内存，这是自动垃圾回收；还有一种是主动垃圾回收，通过显式调用 runtime.GC()来实现。&lt;/p&gt;
&lt;p&gt;通过调用 runtime.GC() 函数可以显式的触发 GC，这在某些的场景下非常有用，比如当内存资源不足时调用 runtime.GC()，它会在此函数执行的点上立即释放一大片内存，但此时程序可能会有短时的性能下降（因为 GC 进程在执行）。&lt;/p&gt;
&lt;p&gt;下面代码中的func (p *Person) NewOpen()在某些情况下非常有必要这样处理，比如某些资源占用申请，开发人员可能忘记使用defer Close()来销毁处理，但通过SetFinalizer，如果GC自动运行或者手动运行GC，则都能及时销毁这些资源，释放占用的内存而避免内存泄漏。&lt;/p&gt;
&lt;p&gt;GC过程中重要的函数func SetFinalizer(obj interface{}, finalizer interface{})有两个参数，参数一：obj必须是指针类型。参数二：finalizer是一个函数，其参数类型是obj的类型，其没有返回值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;log&quot;
	&quot;runtime&quot;
	&quot;time&quot;
)

type Person struct {
	Name string
	Age  int
}

func (p *Person) Close() {
	p.Name = &quot;NewName&quot;
	log.Println(p)
	log.Println(&quot;Close&quot;)
}

func (p *Person) NewOpen() {
	log.Println(&quot;Init&quot;)
	runtime.SetFinalizer(p, (*Person).Close)
}

func Tt(p *Person) {
	p.Name = &quot;NewName&quot;
	log.Println(p)
	log.Println(&quot;Tt&quot;)
}

// 查看内存情况
func Mem(m *runtime.MemStats) {
	runtime.ReadMemStats(m)
	log.Printf(&quot;%d Kb\n&quot;, m.Alloc/1024)
}

func main() {
	var m runtime.MemStats
	Mem(&amp;amp;m)

	var p *Person = &amp;amp;Person{Name: &quot;lee&quot;, Age: 4}
	p.NewOpen()
	log.Println(&quot;Gc完成第一次&quot;)
	log.Println(&quot;p:&quot;, p)
	runtime.GC()
	time.Sleep(time.Second * 5)
	Mem(&amp;amp;m)

	var p1 *Person = &amp;amp;Person{Name: &quot;Goo&quot;, Age: 9}
	runtime.SetFinalizer(p1, Tt)
	log.Println(&quot;Gc完成第二次&quot;)
	time.Sleep(time.Second * 2)
	runtime.GC()
	time.Sleep(time.Second * 2)
	Mem(&amp;amp;m)

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_23_sync.md&quot;&gt;第二十三章 同步与锁&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_25_oo.md&quot;&gt;第二十五章 面向对象&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第二十三章 同步与锁</title><link>https://blog.wemang.com/posts/go/study/42_23_sync/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_23_sync/</guid><pubDate>Mon, 05 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;23.1 同步锁&lt;/h2&gt;
&lt;p&gt;Go语言包中的sync包提供了两种锁类型：sync.Mutex和sync.RWMutex，前者是互斥锁，后者是读写锁。&lt;/p&gt;
&lt;p&gt;互斥锁是传统的并发程序对共享资源进行访问控制的主要手段，在Go中，似乎更推崇由channel来实现资源共享和通信。它由标准库代码包sync中的Mutex结构体类型代表。只有两个公开方法：调用Lock（）获得锁，调用unlock（）释放锁。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用Lock()加锁后，不能再继续对其加锁（同一个goroutine中，即：同步调用），否则会panic。只有在unlock()之后才能再次Lock()。异步调用Lock()，是正当的锁竞争，当然不会有panic了。适用于读写不确定场景，即读写次数没有明显的区别，并且只允许只有一个读或者写的场景，所以该锁也叫做全局锁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;func (m *Mutex) Unlock()用于解锁m，如果在使用Unlock()前未加锁，就会引起一个运行错误。已经锁定的Mutex并不与特定的goroutine相关联，这样可以利用一个goroutine对其加锁，再利用其他goroutine对其解锁。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;建议：同一个互斥锁的成对锁定和解锁操作放在同一层次的代码块中。
使用锁的经典模式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var lck sync.Mutex
func foo() {
    lck.Lock() 
    defer lck.Unlock()
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;lck.Lock()会阻塞直到获取锁，然后利用defer语句在函数返回时自动释放锁。&lt;/p&gt;
&lt;p&gt;下面代码通过3个goroutine来体现sync.Mutex 对资源的访问控制特征：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func main() {
	wg := sync.WaitGroup{}

	var mutex sync.Mutex
	fmt.Println(&quot;Locking  (G0)&quot;)
	mutex.Lock()
	fmt.Println(&quot;locked (G0)&quot;)
	wg.Add(3)

	for i := 1; i &amp;lt; 4; i++ {
		go func(i int) {
			fmt.Printf(&quot;Locking (G%d)\n&quot;, i)
			mutex.Lock()
			fmt.Printf(&quot;locked (G%d)\n&quot;, i)

			time.Sleep(time.Second * 2)
			mutex.Unlock()
			fmt.Printf(&quot;unlocked (G%d)\n&quot;, i)
			wg.Done()
		}(i)
	}

	time.Sleep(time.Second * 5)
	fmt.Println(&quot;ready unlock (G0)&quot;)
	mutex.Unlock()
	fmt.Println(&quot;unlocked (G0)&quot;)

	wg.Wait()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：
Locking  (G0)
locked (G0)
Locking (G1)
Locking (G3)
Locking (G2)
ready unlock (G0)
unlocked (G0)
locked (G1)
unlocked (G1)
locked (G3)
locked (G2)
unlocked (G3)
unlocked (G2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过程序执行结果我们可以看到，当有锁释放时，才能进行lock动作，G0锁释放时，才有后续锁释放的可能，这里是G1抢到释放机会。&lt;/p&gt;
&lt;p&gt;Mutex也可以作为struct的一部分，这样这个struct就会防止被多线程更改数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

type Book struct {
	BookName string
	L        *sync.Mutex
}

func (bk *Book) SetName(wg *sync.WaitGroup, name string) {
	defer func() {
		fmt.Println(&quot;Unlock set name:&quot;, name)
		bk.L.Unlock()
		wg.Done()
	}()

	bk.L.Lock()
	fmt.Println(&quot;Lock set name:&quot;, name)
	time.Sleep(1 * time.Second)
	bk.BookName = name
}

func main() {
	bk := Book{}
	bk.L = new(sync.Mutex)
	wg := &amp;amp;sync.WaitGroup{}
	books := []string{&quot;《三国演义》&quot;, &quot;《道德经》&quot;, &quot;《西游记》&quot;}
	for _, book := range books {
		wg.Add(1)
		go bk.SetName(wg, book)
	}

	wg.Wait()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：

Lock set name: 《西游记》
Unlock set name: 《西游记》
Lock set name: 《三国演义》
Unlock set name: 《三国演义》
Lock set name: 《道德经》
Unlock set name: 《道德经》
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;23.2 读写锁&lt;/h2&gt;
&lt;p&gt;读写锁是分别针对读操作和写操作进行锁定和解锁操作的互斥锁。在Go语言中，读写锁由结构体类型sync.RWMutex代表。&lt;/p&gt;
&lt;p&gt;基本遵循原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;写锁定情况下，对读写锁进行读锁定或者写锁定，都将阻塞；而且读锁与写锁之间是互斥的；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读锁定情况下，对读写锁进行写锁定，将阻塞；加读锁时不会阻塞；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对未被写锁定的读写锁进行写解锁，会引发panic；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对未被读锁定的读写锁进行读解锁的时候也会引发panic；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;写解锁在进行的同时会试图唤醒所有因进行读锁定而被阻塞的协程；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;读解锁在进行的时候则会试图唤醒一个因进行写锁定而被阻塞的协程。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与互斥锁类似，sync.RWMutex类型的零值就已经是立即可用的读写锁了。在此类型的方法集合中包含了两对方法，即：&lt;/p&gt;
&lt;p&gt;RWMutex提供四个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (*RWMutex) Lock // 写锁定
func (*RWMutex) Unlock // 写解锁

func (*RWMutex) RLock // 读锁定
func (*RWMutex) RUnlock // 读解锁

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package main

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

var m *sync.RWMutex

func main() {
	wg := sync.WaitGroup{}
	wg.Add(20)
	var rwMutex sync.RWMutex
	Data := 0
	for i := 0; i &amp;lt; 10; i++ {
		go func(t int) {
			rwMutex.RLock()
			defer rwMutex.RUnlock()
			fmt.Printf(&quot;Read data: %v\n&quot;, Data)
			wg.Done()
			time.Sleep(2 * time.Second)
			// 这句代码第一次运行后，读解锁。
			// 循环到第二个时，读锁定后，这个协程就没有阻塞，同时读成功。
		}(i)

		go func(t int) {
			rwMutex.Lock()
			defer rwMutex.Unlock()
			Data += t
			fmt.Printf(&quot;Write Data: %v %d \n&quot;, Data, t)
			wg.Done() 

			// 这句代码让写锁的效果显示出来，写锁定下是需要解锁后才能写的。
			time.Sleep(2 * time.Second)		
		}(i)
	}
	time.Sleep(5 * time.Second)
	wg.Wait()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;23.3 sync.WaitGroup&lt;/h2&gt;
&lt;p&gt;前面例子中我们有使用WaitGroup，它用于线程同步，WaitGroup等待一组线程集合完成，才会继续向下执行。 主线程(goroutine)调用Add来设置等待的线程(goroutine)数量。 然后每个线程(goroutine)运行，并在完成后调用Done。 同时，Wait用来阻塞，直到所有线程(goroutine)完成才会向下执行。Add(-1)和Done()效果一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;sync&quot;
)

func main() {
	var wg sync.WaitGroup
	for i := 0; i &amp;lt; 10; i++ {
		wg.Add(1)
		go func(t int) {
			defer wg.Done()
			fmt.Println(t)
		}(i)
	}
	wg.Wait()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;23.4 sync.Once&lt;/h2&gt;
&lt;p&gt;sync.Once.Do(f func())能保证once只执行一次,这个sync.Once块只会执行一次。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

var once sync.Once

func main() {

	for i, v := range make([]string, 10) {
		once.Do(onces)
		fmt.Println(&quot;v:&quot;, v, &quot;---i:&quot;, i)
	}

	for i := 0; i &amp;lt; 10; i++ {

		go func(i int) {
			once.Do(onced)
			fmt.Println(i)
		}(i)
	}
	time.Sleep(4000)
}
func onces() {
	fmt.Println(&quot;onces&quot;)
}
func onced() {
	fmt.Println(&quot;onced&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;23.5 sync.Map&lt;/h2&gt;
&lt;p&gt;随着Go1.9的发布，有了一个新的特性，那就是sync.map，它是原生支持并发安全的map。虽然说普通map并不是线程安全（或者说并发安全），但一般情况下我们还是使用它，因为这足够了；只有在涉及到线程安全，再考虑sync.map。&lt;/p&gt;
&lt;p&gt;但由于sync.Map的读取并不是类型安全的，所以我们在使用Load读取数据的时候我们需要做类型转换。&lt;/p&gt;
&lt;p&gt;sync.Map的使用上和map有较大差异，详情见代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;sync&quot;
)

func main() {
	var m sync.Map

	//Store
	m.Store(&quot;name&quot;, &quot;Joe&quot;)
	m.Store(&quot;gender&quot;, &quot;Male&quot;)

	//LoadOrStore
	//若key不存在，则存入key和value，返回false和输入的value
	v, ok := m.LoadOrStore(&quot;name1&quot;, &quot;Jim&quot;)
	fmt.Println(ok, v) //false Jim

	//若key已存在，则返回true和key对应的value，不会修改原来的value
	v, ok = m.LoadOrStore(&quot;name&quot;, &quot;aaa&quot;)
	fmt.Println(ok, v) //true Joe

	//Load
	v, ok = m.Load(&quot;name&quot;)
	if ok {
		fmt.Println(&quot;key存在，值是： &quot;, v)
	} else {
		fmt.Println(&quot;key不存在&quot;)
	}

	//Range
	//遍历sync.Map
	f := func(k, v interface{}) bool {
		fmt.Println(k, v)
		return true
	}
	m.Range(f)

	//Delete
	m.Delete(&quot;name1&quot;)
	fmt.Println(m.Load(&quot;name1&quot;))

}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序运行输出：

false Jim
true Joe
key存在，值是：  Joe
name Joe
gender Male
name1 Jim
&amp;lt;nil&amp;gt; false

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_22_channel.md&quot;&gt;第二十二章 通道(channel)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_24_pointer.md&quot;&gt;第二十四章 指针和内存&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第二十二章 通道(channel)</title><link>https://blog.wemang.com/posts/go/study/42_22_channel/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_22_channel/</guid><pubDate>Sun, 04 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;22.1 通道(channel)&lt;/h2&gt;
&lt;p&gt;Go 奉行通过通信来共享内存，而不是共享内存来通信。所以，&lt;strong&gt;channel 是协程之间互相通信的通道&lt;/strong&gt;，协程之间可以通过它发送消息和接收消息。&lt;/p&gt;
&lt;p&gt;通道是进程内的通信方式，因此通过通道传递对象的行为与函数调用时参数传递行为比较一致，比如也可以传递指针等。&lt;/p&gt;
&lt;p&gt;通道消息传递与消息类型也有关系，一个通道只能传递（发送send或接收receive）类型的值，这需要在声明通道时指定。&lt;/p&gt;
&lt;p&gt;默认情况下，通道是阻塞的 (叫做无缓冲的通道)。&lt;/p&gt;
&lt;p&gt;使用make来建立一个通道：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var channel chan int = make(chan int)
// 或
channel := make(chan int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go中通道可以是发送（send）、接收（receive）、同时发送（send）和接收（receive）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义接收的通道
receive_only := make (&amp;lt;-chan int)
 
// 定义发送的通道
send_only := make (chan&amp;lt;- int)

// 可同时发送接收
send_receive := make (chan int)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;chan&amp;lt;-&lt;/code&gt; 表示数据进入通道，要把数据写进通道，对于调用者就是发送。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;-chan&lt;/code&gt; 表示数据从通道出来，对于调用者就是得到通道的数据，当然就是接收。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;定义只发送或只接收的通道意义不大，一般用于在参数传递中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func main() {
	c := make(chan int) // 不使用带缓冲区的通道
	go send(c)
	go recv(c)
	time.Sleep(3 * time.Second)
close(c)
}

// 只能向chan里send数据
func send(c chan&amp;lt;- int) {
	for i := 0; i &amp;lt; 10; i++ {

		fmt.Println(&quot;send ready &quot;, i)
		c &amp;lt;- i
		fmt.Println(&quot;send &quot;, i)
	}
}

// 只能接收通道中的数据
func recv(c &amp;lt;-chan int) {
	for i := range c {
		fmt.Println(&quot;received &quot;, i)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：

send ready  0
send  0
send ready  1
received  0
received  1
send  1
send ready  2
send  2
send ready  3
received  2
received  3
send  3
send ready  4
send  4
send ready  5
received  4
received  5
send  5
send ready  6
send  6
send ready  7
received  6
received  7
send  7
send ready  8
send  8
send ready  9
received  8
received  9
send  9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果上我们可以发现一个现象，往通道发送数据后，这个数据如果没有被取走，通道是阻塞的，也就是不能继续向通道里面发送数据。上面代码中，我们没有指定通道缓冲区的大小，默认情况下是阻塞的。&lt;/p&gt;
&lt;p&gt;我们可以建立带缓冲区的通道：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c := make(chan int, 1024)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们把前面的程序修改下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func main() {
	c := make(chan int, 10) // 使用带缓冲区的通道
	go send(c)
	go recv(c)
	time.Sleep(3 * time.Second)
	close(c)
}

// 只能向chan里send发送数据
func send(c chan&amp;lt;- int) {
	for i := 0; i &amp;lt; 10; i++ {

		fmt.Println(&quot;send ready &quot;, i)
		c &amp;lt;- i
		fmt.Println(&quot;send &quot;, i)
	}
}

// 只能接收通道中的数据
func recv(c &amp;lt;-chan int) {
	for i := range c {
		fmt.Println(&quot;received &quot;, i)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：

send ready  0
send  0
send ready  1
send  1
send ready  2
send  2
send ready  3
send  3
send ready  4
send  4
send ready  5
received  0
received  1
received  2
received  3
received  4
received  5
send  5
send ready  6
send  6
send ready  7
send  7
send ready  8
send  8
send ready  9
send  9
received  6
received  7
received  8
received  9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从运行结果我们可以看到（每次执行顺序不一定相同，协程运行导致的原因），带有缓冲区的通道，在缓冲区有数据而未填满前，读取不会出现阻塞的情况。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无缓冲的通道（unbuffered channel）是指在接收前没有能力保存任何值的通道。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种类型的通道要求发送协程和接收协程同时准备好，才能完成发送和接收操作。如果两个协程没有同时准备好，通道会导致先执行发送或接收操作的协程阻塞等待。&lt;/p&gt;
&lt;p&gt;这种对通道进行发送和接收的交互行为本身就是同步的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有缓冲的通道（buffered channel）是一种在被接收前能存储一个或者多个值的通道。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种类型的通道并不强制要求协程之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时，接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时，发送动作才会阻塞。&lt;/p&gt;
&lt;p&gt;这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同：无缓冲的通道保证进行发送和接收的协程会在同一时间进行数据交换；有缓冲的通道没有这种保证。&lt;/p&gt;
&lt;p&gt;如果给定了一个缓冲区容量，通道就是异步的。只要缓冲区有未使用空间用于发送数据，或还包含可以接收的数据，那么其通信就会无阻塞地进行。&lt;/p&gt;
&lt;p&gt;可以通过内置的close函数来关闭通道实现。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;通道不需要经常去关闭，只有当没有任何可发送数据时才去关闭通道；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭通道后，无法向通道再发送数据(引发panic 错误后导致接收立即返回零值)；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭通道后，可以继续向通道接收数据，不能继续发送数据；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于nil 通道，无论收发都会被阻塞。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_21_goroutine.md&quot;&gt;第二十一章 协程(goroutine)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_23_sync.md&quot;&gt;第二十三章 同步与锁&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。
=======&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;《Go语言四十二章经》第二十二章 通道(channel)&lt;/h1&gt;
&lt;p&gt;作者：李骁(ffhelicopter)&lt;/p&gt;
&lt;h2&gt;22.1 通道(channel)&lt;/h2&gt;
&lt;p&gt;Go 奉行通过通信来共享内存，而不是共享内存来通信。所以，&lt;strong&gt;channel 是协程之间互相通信的通道&lt;/strong&gt;，协程之间可以通过它发送消息和接收消息。&lt;/p&gt;
&lt;p&gt;通道是进程内的通信方式，因此通过通道传递对象的行为与函数调用时参数传递行为比较一致，比如也可以传递指针等。&lt;/p&gt;
&lt;p&gt;通道消息传递与消息类型也有关系，一个通道只能传递（发送send或接收receive）类型的值，这需要在声明通道时指定。&lt;/p&gt;
&lt;p&gt;默认情况下，通道是阻塞的 (叫做无缓冲的通道)。&lt;/p&gt;
&lt;p&gt;使用make来建立一个通道：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var channel chan int = make(chan int)
// 或
channel := make(chan int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go中通道可以是发送（send）、接收（receive）、同时发送（send）和接收（receive）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义接收的通道
receive_only := make (&amp;lt;-chan int)
 
// 定义发送的通道
send_only := make (chan&amp;lt;- int)

// 可同时发送接收
send_receive := make (chan int)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;chan&amp;lt;-&lt;/code&gt; 表示数据进入通道，要把数据写进通道，对于调用者就是发送。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;-chan&lt;/code&gt; 表示数据从通道出来，对于调用者就是得到通道的数据，当然就是接收。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;定义只发送或只接收的通道意义不大，一般用于在参数传递中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func main() {
	c := make(chan int) // 不使用带缓冲区的通道
	go send(c)
	go recv(c)
	time.Sleep(3 * time.Second)
close(c)
}

// 只能向chan里send数据
func send(c chan&amp;lt;- int) {
	for i := 0; i &amp;lt; 10; i++ {

		fmt.Println(&quot;send ready &quot;, i)
		c &amp;lt;- i
		fmt.Println(&quot;send &quot;, i)
	}
}

// 只能接收通道中的数据
func recv(c &amp;lt;-chan int) {
	for i := range c {
		fmt.Println(&quot;received &quot;, i)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：

send ready  0
send  0
send ready  1
received  0
received  1
send  1
send ready  2
send  2
send ready  3
received  2
received  3
send  3
send ready  4
send  4
send ready  5
received  4
received  5
send  5
send ready  6
send  6
send ready  7
received  6
received  7
send  7
send ready  8
send  8
send ready  9
received  8
received  9
send  9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果上我们可以发现一个现象，往通道发送数据后，这个数据如果没有被取走，通道是阻塞的，也就是不能继续向通道里面发送数据。上面代码中，我们没有指定通道缓冲区的大小，默认情况下是阻塞的。&lt;/p&gt;
&lt;p&gt;我们可以建立带缓冲区的通道：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c := make(chan int, 1024)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们把前面的程序修改下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func main() {
	c := make(chan int, 10) // 使用带缓冲区的通道
	go send(c)
	go recv(c)
	time.Sleep(3 * time.Second)
	close(c)
}

// 只能向chan里send发送数据
func send(c chan&amp;lt;- int) {
	for i := 0; i &amp;lt; 10; i++ {

		fmt.Println(&quot;send ready &quot;, i)
		c &amp;lt;- i
		fmt.Println(&quot;send &quot;, i)
	}
}

// 只能接收通道中的数据
func recv(c &amp;lt;-chan int) {
	for i := range c {
		fmt.Println(&quot;received &quot;, i)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：

send ready  0
send  0
send ready  1
send  1
send ready  2
send  2
send ready  3
send  3
send ready  4
send  4
send ready  5
received  0
received  1
received  2
received  3
received  4
received  5
send  5
send ready  6
send  6
send ready  7
send  7
send ready  8
send  8
send ready  9
send  9
received  6
received  7
received  8
received  9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从运行结果我们可以看到（每次执行顺序不一定相同，协程运行导致的原因），带有缓冲区的通道，在缓冲区有数据而未填满前，读取不会出现阻塞的情况。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无缓冲的通道（unbuffered channel）是指在接收前没有能力保存任何值的通道。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种类型的通道要求发送协程和接收协程同时准备好，才能完成发送和接收操作。如果两个协程没有同时准备好，通道会导致先执行发送或接收操作的协程阻塞等待。&lt;/p&gt;
&lt;p&gt;这种对通道进行发送和接收的交互行为本身就是同步的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有缓冲的通道（buffered channel）是一种在被接收前能存储一个或者多个值的通道。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种类型的通道并不强制要求协程之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时，接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时，发送动作才会阻塞。&lt;/p&gt;
&lt;p&gt;这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同：无缓冲的通道保证进行发送和接收的协程会在同一时间进行数据交换；有缓冲的通道没有这种保证。&lt;/p&gt;
&lt;p&gt;如果给定了一个缓冲区容量，通道就是异步的。只要缓冲区有未使用空间用于发送数据，或还包含可以接收的数据，那么其通信就会无阻塞地进行。&lt;/p&gt;
&lt;p&gt;可以通过内置的close函数来关闭通道实现。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;通道不需要经常去关闭，只有当没有任何可发送数据时才去关闭通道；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭通道后，无法向通道再发送数据(引发panic 错误后导致接收立即返回零值)；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭通道后，可以继续向通道接收数据，不能继续发送数据；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于nil 通道，无论收发都会被阻塞。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_21_goroutine.md&quot;&gt;第二十一章 协程(goroutine)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_23_sync.md&quot;&gt;第二十三章 同步与锁&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。
联系邮箱：roteman@163.com&lt;/p&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;6286e540c31a666070f3d8cd07fb5b8514bc3229&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第二十一章 协程(goroutine)</title><link>https://blog.wemang.com/posts/go/study/42_21_goroutine/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_21_goroutine/</guid><pubDate>Sat, 03 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Concurrency is about dealing with lots of things at once.
Parallelism is about doing lots of things at once.&lt;/p&gt;
&lt;p&gt;并发： 指的是程序的逻辑结构。如果程序代码结构中的某些函数逻辑上可以同时运行，但物理上未必会同时运行。
并行： 并行是指程序的运行状态。并行则指的就是在物理层面也就是使用了不同CPU在执行不同或者相同的任务。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;21.1 并发&lt;/h2&gt;
&lt;p&gt;并发是在同一时间处理多件事情。并行是在同一时间做多件事情。并发的目的在于把当个 CPU 的利用率使用到最高。并行则需要多核 CPU 的支持。&lt;/p&gt;
&lt;p&gt;Go 语言在语言层面上支持了并发，goroutine是Go语言提供的一种用户态线程，有时我们也称之为协程。所谓的协程，某种程度上也可以叫做轻量线程，它不由系统而由应用程序创建和管理，因此使用开销较低（一般为4K）。我们可以创建很多的协程，并且它们跑在同一个内核线程之上的时候，就需要一个调度器来维护这些协程，确保所有的协程都能使用CPU，并且是尽可能公平地使用CPU资源。&lt;/p&gt;
&lt;p&gt;调度器的主要有4个重要部分，分别是M、G、P、Sched。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;M (work thread)  代表了系统线程内核线程，由操作系统管理。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;P (processor)    衔接M和G的调度上下文，它负责将等待执行的G与M对接。P的数量可以通过GOMAXPROCS()来设置，它其实也就代表了真正的并发度，即有多少个goroutine可以同时运行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;G (goroutine)    协程的实体，包括了调用栈，重要的调度信息，例如channel等。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在操作系统的内核线程和编程语言的用户线程之间，实际上存在3种线程对应模型，也就是：1:1，1:N，M:N。&lt;/p&gt;
&lt;p&gt;N:1 多个（N）用户线程始终在一个内核线程上跑，context上下文切换很快，但是无法真正的利用多核。
1:1 一个用户线程就只在一个内核线程上跑，这时可以利用多核，但是上下文切换很慢，切换效率很低。
M:N 多个协程在多个内核线程上跑，这个可以集齐上面两者的优势，但是无疑增加了调度的难度。&lt;/p&gt;
&lt;p&gt;M:N 综合两种方式（N:1，1:1）的优势。多个协程可以在多个内核线程上处理。既能快速切换上下文，也能利用多核的优势，而Go正是选择这种实现方式。&lt;/p&gt;
&lt;p&gt;Go 语言中的协程是运行在多核CPU中的(通过runtime.GOMAXPROCS(1)设定CPU核数)。 实际中运行的CPU核数未必会和实际物理CPU数相吻合。&lt;/p&gt;
&lt;p&gt;每个协程都会被一个特定的P(某个CPU)选定维护，而M(物理计算资源)每次挑选一个有效P，然后执行P中的协程。&lt;/p&gt;
&lt;p&gt;每个P会将自己所维护的协程放到一个G队列中，其中就包括了协程堆栈信息，是否可执行信息等等。&lt;/p&gt;
&lt;p&gt;默认情况下，P的数量与实际物理CPU的数量相等。当我们通过循环来创建协程时，协程会被分配到不同的G队列中。 而M的数量又不是唯一的，当M随机挑选P时，也就等同随机挑选了协程。&lt;/p&gt;
&lt;p&gt;所以，当我们碰到多个协程的执行顺序不是我们想象的顺序时就可以理解了，因为协程进入P管理的队列G是带有随机性的。&lt;/p&gt;
&lt;p&gt;P的数量由runtime.GOMAXPROCS(1)所设定，通常来说它是和内核数对应，例如在4Core的服务器上会启动4个线程。G会有很多个，每个P会将协程从一个就绪的队列中做Pop操作，为了减小锁的竞争，通常情况下每个P会负责一个队列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;runtime.NumCPU()        // 返回当前CPU内核数
runtime.GOMAXPROCS(2)  // 设置运行时最大可执行CPU数
runtime.NumGoroutine() // 当前正在运行的协程 数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;P维护着这个队列（称之为runqueue），Go语言里，启动一个协程很容易：go function 就行，所以每有一个go语句被执行，runqueue队列就在其末尾加入一个协程，在下一个调度点，就从runqueue中取出一个协程执行。&lt;/p&gt;
&lt;p&gt;假如有两个M，即两个内核线程，分别对应一个P，每一个P调度一个G队列。如此一来，就组成的协程运行时的基本结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当有一个M返回时，它必须尝试取得一个P来运行协程，一般情况下，它会从其他的OS Thread线程那里窃取一个P过来，如果没有拿到，它就把协程放在一个global runqueue里，然后自己进入线程缓存里。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果某个P所分配的任务G很快就执行完了，这会导致多个队列存在不平衡，会从其他队列中截取一部分协程到P上进行调度。一般来说，如果P从其他的P那里要取任务的话，一般就取run queue的一半，这就确保了每个内核线程都能充分的使用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当一个内核线程被阻塞时，P可以转而投奔另一个内核线程。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以运行下面代码体验下Go语言中通过设定runtime.GOMAXPROCS(2) ，也即手动指定CPU运行的核数，来体验多核CPU在并发处理时的威力。不得不提，递归函数的计算很费CPU和内存，运行时可以根据电脑配置修改循环或递归数量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

var quit chan int = make(chan int)

func loop() {
	for i := 0; i &amp;lt; 1000; i++ {
		Factorial(uint64(1000))
	}
	quit &amp;lt;- 1
}
func Factorial(n uint64) (result uint64) {
	if n &amp;gt; 0 {
		result = n * Factorial(n-1)
		return result
	}
	return 1
}

var wg1, wg2 sync.WaitGroup

func main() {
	fmt.Println(&quot;1:&quot;, time.Now())
	fmt.Println(runtime.NumCPU()) // 默认CPU核数
	a := 5000
	for i := 1; i &amp;lt;= a; i++ {
		wg1.Add(1)
		go loop()
	}

	for i := 0; i &amp;lt; a; i++ {
		select {
		case &amp;lt;-quit:
			wg1.Done()
		}
	}
	fmt.Println(&quot;2:&quot;, time.Now())
	wg1.Wait()

	fmt.Println(&quot;3:&quot;, time.Now())
	runtime.GOMAXPROCS(2) // 设置执行使用的核数
	a = 5000
	for i := 1; i &amp;lt;= a; i++ {
		wg2.Add(1)
		go loop()
	}

	for i := 0; i &amp;lt; a; i++ {
		select {
		case &amp;lt;-quit:
			wg2.Done()
		}
	}

	fmt.Println(&quot;4:&quot;, time.Now())
	wg2.Wait()
	fmt.Println(&quot;5:&quot;, time.Now())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我的测试电脑CPU默认是4核，对比手动设置CPU在2核时的运行耗时，4核耗时约8秒，2核约14秒，当然这是一种比较理想化的测试，因为阶乘很快导致unit64为0，所以这个测试并不严谨，但从中我们仍然可以体验到Go语言在处理并发时代码之简单，控制之方便。&lt;/p&gt;
&lt;p&gt;在实际中运行速度延缓可能不一定仅仅是由于CPU的竞争，可能还有内存或者I/O的原因导致的，我们需要根据情况仔细分析。&lt;/p&gt;
&lt;p&gt;最后，runtime.Gosched()用于让出CPU时间片，让出当前协程的执行权限，调度器安排其他等待的任务运行，并在下次某个时候从该位置恢复执行。&lt;/p&gt;
&lt;h2&gt;21.2 goroutine&lt;/h2&gt;
&lt;p&gt;在Go语言中，协程的使用很简单，直接在函数（代码块）前加上关键字 go 即可。go关键字就是用来创建一个协程的，后面的代码块就是这个协程需要执行的代码逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func main() {
	for i := 1; i &amp;lt; 10; i++ {
		go func(i int) {
			fmt.Println(i)
		}(i)
	}
	// 暂停一会，保证打印全部结束
	time.Sleep(1e9)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;time.Sleep(1e9)让主程序不会马上退出，以便让协程运行完成，避免主程序退出时协程未处理完成甚至没有开始运行。&lt;/p&gt;
&lt;p&gt;有关于协程之间的通信以及协程与主线程的控制以及多个协程的管理和控制，我们后续通过channel、context以及锁来进一步说明。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_20_method.md&quot;&gt;第二十章 方法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_22_channel.md&quot;&gt;第二十二章 通道(channel)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第二十章 方法</title><link>https://blog.wemang.com/posts/go/study/42_20_method/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_20_method/</guid><pubDate>Fri, 02 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在前面我们讲了结构体（struct）和接口（interface），在里面也提到过方法，但没有详细介绍方法（Method）。在这一章里，我们来仔细看看方法有那些奇妙之处呢？&lt;/p&gt;
&lt;h2&gt;20.1 方法的定义&lt;/h2&gt;
&lt;p&gt;在 Go 语言中，结构体就像是类的一种简化形式，那么面向对象程序员可能会问：类的方法在哪里呢？在 Go 语言中有一个概念，它和方法有着同样的名字，并且大体上意思相近。&lt;/p&gt;
&lt;p&gt;Go 语言中方法和函数在形式上很像，它是作用在接收器（receiver）上的一个函数，接收器是某种类型的变量。因此方法是一种特殊类型的函数，方法只是比函数多了一个接收器（receiver），当然在接口中定义的函数我们也称为方法（因为最终还是要通过绑定到类型来实现）。&lt;/p&gt;
&lt;p&gt;正是因为有了接收器，方法才可以作用于接收器的类型（变量）上，类似于面向对象中类的方法可以作用于类属性上。&lt;/p&gt;
&lt;p&gt;定义方法的一般格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在方法名之前，func 关键字之后的括号中指定接收器 receiver。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type A struct {
	Face int
}

func (a A) f() {
	fmt.Println(&quot;hi &quot;, a.Face)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中，我们定义了结构体 A ，注意f()就是 A 的方法，(a A)表示接收器。a 是 A的实例，f()是它的方法名，方法调用遵循传统的 object.name 即选择器符号：a.f()。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接收器(receiver)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收器类型除了不能是指针类型或接口类型外，可以是其他任何类型，不仅仅是结构体类型，也可以是函数类型，还可以是 int、bool、string 等等为基础的自定义类型。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type Human struct {
	name   string // 姓名
	Gender string // 性别
	Age    int    // 年龄
	string        // 匿名字段
}

func (h Human) print() { // 值方法
	fmt.Println(&quot;Human:&quot;, h)
}

type MyInt int

func (m MyInt) print() { // 值方法
	fmt.Println(&quot;MyInt:&quot;, m)
}

func main() {
	//使用new方式
	hu := new(Human)
	hu.name = &quot;Titan&quot;
	hu.Gender = &quot;男&quot;
	hu.Age = 14
	hu.string = &quot;Student&quot;
	hu.print()

	// 指针变量
	mi := new(MyInt)
	mi.print()

	// 使用结构体字面量赋值
	hum := Human{&quot;Hawking&quot;, &quot;男&quot;, 14, &quot;Monitor&quot;}
	hum.print()

	// 值变量
	myi := MyInt(99)
	myi.print()
}

程序输出：
Human: {Titan 男 14 Student}
MyInt: 0
Human: {Hawking 男 14 Monitor}
MyInt: 99
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;接收器不能是一个接口类型，因为接口是一个抽象定义，但是方法却是具体实现；如果这样做会引发一个编译错误：invalid receiver type…。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type printer interface {
	print()
}

func (p printer) print() { //  invalid receiver type printer (printer is an interface type)
	fmt.Println(&quot;printer:&quot;, p)
}
func main() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;接收器不能是一个指针类型，但是它可以是任何其他允许类型的指针。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type MyInt int

type Q *MyInt

func (q Q) print() { // invalid receiver type Q (Q is a pointer type)
	fmt.Println(&quot;Q:&quot;, q)
}

func main() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接收器不能是指针类型，但可以是类型的指针，有点绕口。下面我们看个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type MyInt int

func (mi *MyInt) print() { // 指针接收器，指针方法
	fmt.Println(&quot;MyInt:&quot;, *mi)
}
func (mi MyInt) echo() { // 值接收器，值方法
	fmt.Println(&quot;MyInt:&quot;, mi)
}
func main() {
	i := MyInt(9)
	i.print()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果有类型T，方法的接收器为(t T)时我们称为值接收器，该方法称为值方法；方法的接收器为(t *T)时我们称为指针接收器，该方法称为指针方法。&lt;/p&gt;
&lt;p&gt;类型 T（或 *T）上的所有方法的集合叫做类型 T（或 *T）的方法集。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于接收器的命名&lt;/p&gt;
&lt;p&gt;社区约定的接收器命名是类型的一个或两个字母的缩写(像 c 或者 cl 对于 Client)。不要使用泛指的名字像是 me，this 或者 self，也不要使用过度描述的名字，简短即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;方法表达式与方法值&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在Go语言中，方法调用的方式如下：如有类型X的变量x，m()是其方法，则方法有效调用方式是x.m()，如果x是指针变量，则x.m()实际上是(&amp;amp;x).m()的简写。所以我们看到指针方法的调用写成x.m()，这其实是一种语法糖。&lt;/p&gt;
&lt;p&gt;这里我们了解下Go语言的选择器（selector），如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x.f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码表示如果x不是包名，则表示是x（或* x）的f（字段或方法）。标识符f（字段或方法）称为选择器(selector)，选择器不能是空白标识符。选择器表达式的类型是f的类型。&lt;/p&gt;
&lt;p&gt;选择器f可以表示类型T的字段或方法，或者指嵌入字段T的字段或方法f。遍历到f的嵌入字段的层数被称为其在T中的深度。在T中声明的字段或方法f的深度为零。在T中的嵌入字段A中声明的字段或方法f的深度是A中的f的深度加1。&lt;/p&gt;
&lt;p&gt;在Go语言中，我们认为方法的显式接收器(explicit receiver)x是方法x.m()的等效函数X.m()的第一个参数，所以x.m()和X.m(x)是等价的，下面我们看看具体例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type T struct {
	a int
}

func (tv T) Mv(a int) int {
	fmt.Printf(&quot;Mv的值是: %d\n&quot;, a)
	return a
} // 值方法

func (tp *T) Mp(f float32) float32 {
	fmt.Printf(&quot;Mp: %f\n&quot;, f)
	return f
} // 指针方法

func main() {
	var t T
	// 下面几种调用方法是等价的
	t.Mv(1)    // 一般调用
	T.Mv(t, 1) // 显式接收器t可以当做为函数的第一个参数
	f0 := t.Mv // 通过选择器（selector）t.Mv将方法值赋值给一个变量 f0
	f0(2)
	T.Mv(t, 3)
	(T).Mv(t, 4)
	f1 := T.Mv // 利用方法表达式(Method Expression) T.Mv 取到函数值
	f1(t, 5)
	f2 := (T).Mv // 利用方法表达式(Method Expression) T.Mv 取到函数值
	f2(t, 6)
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;t.Mv(1)和T.Mv(t, 1)效果是一致的，这里显式接收器t可以当做为等效函数T.Mv()的第一个参数。而在Go语言中，我们可以利用选择器，将方法值(Method Value)取到，并可以将其赋值给其它变量。使用 t.Mv，就可以得到 Mv 方法的方法值，而且这个方法值绑定到了显式接收器（实参）t。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;f0 := t.Mv // 通过选择器将方法值t.Mv赋值给一个变量 f0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了使用选择器取到方法值外，还可以使用方法表达式(Method Expression) 取到函数值(Function Value)。方法表达式(Method Expression)产生的是一个函数值(Function Value)而不是方法值(Method Value)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;f1 := T.Mv // 利用方法表达式(Method Expression) T.Mv 取到函数值
f1(t, 5)
f2 := (T).Mv // 利用方法表达式(Method Expression) T.Mv 取到函数值
f2(t, 6)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数值的第一个参数必须是一个接收器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;f1(t, 5)
f2(t, 6)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面有关选择器，方法表达式，函数值，方法值等概念可以帮助我们更好理解方法，掌握他们可以更好地使用好方法。&lt;/p&gt;
&lt;p&gt;在Go语言中不允许方法重载，因为方法是函数，所以对于一个类型只能有唯一一个特定名称的方法。但是如果基于接收器类型，我们可以通过一种变通的方法，达到这个目的：具有同样名字的方法可以在 2 个或多个不同的接收器类型上存在，比如在同一个包里这么做是允许的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type MyInt1 int
type MyInt2 int

func (a *MyInt1) Add(b int) int { return 0 }
func (a *MyInt2) Add(b int) int { return 0 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;自定义类型方法与匿名嵌入&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Go语言中类型加上它的方法集等价于面向对象中的类。但在 Go 语言中，类型的代码和绑定在它上面的方法集的代码可以不放置在同一个文件中，它们可以保存在同一个包下的其他源文件中。&lt;/p&gt;
&lt;p&gt;下面是在非结构体类型上定义方法的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type MyInt int

func (m MyInt) print() { // 值方法
	fmt.Println(&quot;MyInt:&quot;, m)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：类型和作用在它上面定义的方法必须在同一个包里定义，所以基础类型int、float 等上不能直接定义。&lt;/p&gt;
&lt;p&gt;类型在其他的，或是非本地的包里定义，在它上面定义方法都会发生错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

func (i int) print() { // cannot define new methods on non-local type int
	fmt.Println(&quot;Int:&quot;, i)
}

func main() {
}

程序编译不通过，错误如下：
cannot define new methods on non-local type int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然我们不能直接为非同一包下的类型直接定义方法，但我们可以以这个类型（比如：int 或 float）为基础来自定义新类型，然后再为新类型定义方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type MyInt int

func (m MyInt) print() { // 值方法
	fmt.Println(&quot;MyInt:&quot;, m)
}

func main() {
	myi := MyInt(99)
	myi.print()
}

程序输出：
MyInt: 99
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MyInt类型由int 为基础自定义的，MyInt定义了一个方法print()。&lt;/p&gt;
&lt;p&gt;下面我们再以这个代码为例看看在类型别名下的方法情况，类型别名情况下方法是保留的，但自定义的新类型方法是需要重新定义的，原方法不保留。&lt;/p&gt;
&lt;p&gt;如果我们采用类型别名下面程序可正常运行，Go 1.9及以上版本编译通过：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type MyInt int
type NewInt = MyInt

func (m MyInt) print() { // 值方法
	fmt.Println(&quot;MyInt:&quot;, m)
}

func main() {
	myi := MyInt(99)
	myi.print()

	Ni := NewInt(myi)
	Ni.print()
}

程序输出：
MyInt: 99
MyInt: 99
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但上面代码我们稍微修改，把type NewInt = MyInt 改为type NewInt  MyInt 。一个符号“=”去掉使得NewInt 变为新类型，会报程序错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Ni.print undefined (type NewInt has no field or method print)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为Ni 属于新的自定义类型 NewInt, 它没有定义print()方法，需要另外定义这个方法。&lt;/p&gt;
&lt;p&gt;我们也可以像下面这样将定义好的类型作为匿名类型嵌入在一个新的结构体中。当然新方法只在这个自定义类型上有效。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type Human struct {
	name   string // 姓名
	Gender string // 性别
	Age    int    // 年龄
	string        // 匿名字段
}

type Student struct {
	Human     // 匿名字段
	Room  int // 教室
	int       // 匿名字段
}

func (h Human) String() { // 值方法
	fmt.Println(&quot;Human&quot;)
}

func (s Student) String() { // 值方法
	fmt.Println(&quot;Student&quot;)
}

func (s Student) Print() { // 值方法
	fmt.Println(&quot;Print&quot;)
}

func main() {
	stud := Student{Room: 102, Human: Human{&quot;Hawking&quot;, &quot;男&quot;, 14, &quot;Monitor&quot;\}\}
	stud.String()
	stud.Human.String()
}

程序输出：
Student
Human
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;20.2 函数和方法的区别&lt;/h2&gt;
&lt;p&gt;方法相对于函数多了接收器，这是他们之间最大的区别。&lt;/p&gt;
&lt;p&gt;函数是直接调用，而方法是作用在接收器上，方法需要类型的实例来调用。方法接收器必须有一个显式的名字，这个名字必须在方法中被使用。&lt;/p&gt;
&lt;p&gt;在接收器是指针时，方法可以改变接收器的值（或状态），这点函数也可以做到（当参数作为指针传递，即通过引用调用时，函数也可以改变参数的状态）。&lt;/p&gt;
&lt;p&gt;在 Go 语言中，（接收器）类型关联的方法不写在类型结构里面，就像类那样；耦合更加宽松；类型和方法之间的关联由接收器来建立。&lt;/p&gt;
&lt;p&gt;方法没有和定义的数据类型（结构体）混在一起，方法和数据是正交，而且数据和行为（方法）是相对独立的。&lt;/p&gt;
&lt;h2&gt;20.3 指针方法与值方法&lt;/h2&gt;
&lt;p&gt;有类型T，方法的接收器为(t T)时我们称为值接收器，该方法称为值方法；方法的接收器为(t *T)时我们称为指针接收器，该方法称为指针方法。&lt;/p&gt;
&lt;p&gt;如果想要方法改变接收器的数据，就在接收器的指针上定义该方法；否则，就在普通的值类型上定义方法。这是指针方法和值方法最大的区别。&lt;/p&gt;
&lt;p&gt;下面声明一个 T 类型的变量，并调用其方法 M1() 和 M2() 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type T struct {
	Name string
}

func (t T) M1() {
	t.Name = &quot;name1&quot;
}

func (t *T) M2() {
	t.Name = &quot;name2&quot;
}
func main() {

	t1 := T{&quot;t1&quot;}

	fmt.Println(&quot;M1调用前：&quot;, t1.Name)
	t1.M1()
	fmt.Println(&quot;M1调用后：&quot;, t1.Name)

	fmt.Println(&quot;M2调用前：&quot;, t1.Name)
	t1.M2()
	fmt.Println(&quot;M2调用后：&quot;, t1.Name)

}

程序输出：
M1调用前： t1
M1调用后： t1
M2调用前： t1
M2调用后： name2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见，t1.M2()修改了接收器数据。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;分析：&lt;/p&gt;
&lt;p&gt;分析：
由于调用 t1.M1() 时相当于T.M1(t1)，实参和形参都是类型 T。此时在M1()中的t只是t1的值拷贝，所以M1()的修改影响不到t1。&lt;/p&gt;
&lt;p&gt;同上， t1.M2() =&amp;gt; M2(t1)，这是将 T 类型传给了 *T 类型，Go会取 t1 的地址传进去：M2(&amp;amp;t1)，所以M2()的修改可以影响 t1 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面的例子同时也说明了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; T 类型的变量可以调用M1()和M2()这两个方法。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为对于类型 T，如果在 *T 上存在方法 M2()，并且 t 是这个类型的变量，那么 t.M2() 会被自动转换为 (&amp;amp;t).M2()。&lt;/p&gt;
&lt;p&gt;下面声明一个 *T 类型的变量，并调用方法 M1() 和 M2() 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type T struct {
	Name string
}

func (t T) M1() {
	t.Name = &quot;name1&quot;
}

func (t *T) M2() {
	t.Name = &quot;name2&quot;
}
func main() {

	t2 := &amp;amp;T{&quot;t2&quot;}

	fmt.Println(&quot;M1调用前：&quot;, t2.Name)
	t2.M1()
	fmt.Println(&quot;M1调用后：&quot;, t2.Name)

	fmt.Println(&quot;M2调用前：&quot;, t2.Name)
	t2.M2()
	fmt.Println(&quot;M2调用后：&quot;, t2.Name)

}

程序输出：
M1调用前： t2
M1调用后： t2
M2调用前： t2
M2调用后： name2
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;分析：&lt;/p&gt;
&lt;p&gt;t2.M1() =&amp;gt; M1(t2)，t2 是指针类型，取t2的值并拷贝一份传给M1()。&lt;/p&gt;
&lt;p&gt;t2.M2() =&amp;gt; M2(t2)，都是指针类型，不需要转换。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;*T 类型的变量也可以调用M1()和M2()这两个方法。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面调用我们可以得知：无论你声明方法的接收器是指针接收器还是值接收器，Go都可以帮你隐式转换为正确的方法使用。&lt;/p&gt;
&lt;p&gt;但我们需要记住，值变量只拥有值方法集，而指针变量则同时拥有值方法集和指针方法集。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接口变量上的指针方法与值方法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;无论是T类型变量还是*T类型变量，都可调用值方法或指针方法。但如果是接口变量呢，那么这两个方法都可以调用吗？&lt;/p&gt;
&lt;p&gt;我们添加一个接口看看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

type T struct {
	Name string
}
type Intf interface {
	M1()
	M2()
}

func (t T) M1() {
	t.Name = &quot;name1&quot;
}

func (t *T) M2() {
	t.Name = &quot;name2&quot;
}
func main() {
	var t1 T = T{&quot;t1&quot;}
	t1.M1()
	t1.M2()

	var t2 Intf = t1
	t2.M1()
	t2.M2()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译不通过：&lt;/p&gt;
&lt;p&gt;cannot use t1 (type T) as type Intf in assignment:
T does not implement Intf (M2 method has pointer receiver)&lt;/p&gt;
&lt;p&gt;上面代码中我们看到，var t2 Intf 中，t2是Intf接口类型变量，t1是T类型值变量。上面错误信息中已经明确了T没有实现接口Intf，所以不能直接赋值。这是为什么呢？&lt;/p&gt;
&lt;p&gt;首先这是Go语言的一种规则，具体如下：&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;/ul&gt;
&lt;p&gt;按照上面两条规则的规则一，我们稍微修改下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

type T struct {
	Name string
}
type Intf interface {
	M1()
	M2()
}

func (t T) M1() {
	t.Name = &quot;name1&quot;
}

func (t *T) M2() {
	t.Name = &quot;name2&quot;
}
func main() {

	var t1 T = T{&quot;t1&quot;}
	t1.M1()
	t1.M2()

	var t2 Intf = &amp;amp;t1
	t2.M1()
	t2.M2()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序编译通过。&lt;/p&gt;
&lt;p&gt;程序编译通过。综合起来看，接口类型的变量（实现了该接口的类型变量）调用方法时，我们需要注意方法的接收器，是不是真正实现了接口。结合接口类型断言，我们做下测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type T struct {
	Name string
}
type Intf interface {
	M1()
	M2()
}

func (t T) M1() {
	t.Name = &quot;name1&quot;
	fmt.Println(&quot;M1&quot;)
}

func (t *T) M2() {
	t.Name = &quot;name2&quot;
	fmt.Println(&quot;M2&quot;)
}
func main() {

	var t1 T = T{&quot;t1&quot;}

	// interface{}(t1) 先转为空接口，再使用接口断言
	_, ok1 := interface{}(t1).(Intf)
	fmt.Println(&quot;t1 =&amp;gt; Intf&quot;, ok1)

	_, ok2 := interface{}(t1).(T)
	fmt.Println(&quot;t1 =&amp;gt; T&quot;, ok2)
	t1.M1()
	t1.M2()

	_, ok3 := interface{}(t1).(*T)
	fmt.Println(&quot;t1 =&amp;gt; *T&quot;, ok3)
	t1.M1()
	t1.M2()

	_, ok4 := interface{}(&amp;amp;t1).(Intf)
	fmt.Println(&quot;&amp;amp;t1 =&amp;gt; Intf&quot;, ok4)
	t1.M1()
	t1.M2()

	_, ok5 := interface{}(&amp;amp;t1).(T)
	fmt.Println(&quot;&amp;amp;t1 =&amp;gt; T&quot;, ok5)

	_, ok6 := interface{}(&amp;amp;t1).(*T)
	fmt.Println(&quot;&amp;amp;t1 =&amp;gt; *T&quot;, ok6)
	t1.M1()
	t1.M2()

}


程序输出：
t1 =&amp;gt; Intf false
t1 =&amp;gt; T true
M1
M2
t1 =&amp;gt; *T false
M1
M2
&amp;amp;t1 =&amp;gt; Intf true
M1
M2
&amp;amp;t1 =&amp;gt; T false
&amp;amp;t1 =&amp;gt; *T true
M1
M2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果表明，t1 没有实现Intf方法集，不是Intf接口类型；而&amp;amp;t1 则实现了Intf方法集，是Intf接口类型，可以调用相应方法。t1 这个结构体值变量本身则调用值方法或者指针方法都是可以的，这是因为语法糖存在的原因。&lt;/p&gt;
&lt;p&gt;按照上面的两条规则，那究竟怎么选择是指针接收器还是值接收器呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;何时使用值类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（1）如果接收器是一个 map，func 或者 chan，使用值类型（因为它们本身就是引用类型）。
（2）如果接收器是一个 slice，并且方法不执行 reslice 操作，也不重新分配内存给 slice，使用值类型。
（3）如果接收器是一个小的数组或者原生的值类型结构体类型(比如 time.Time 类型)，而且没有可修改的字段和指针，又或者接收器是一个简单地基本类型像是 int 和 string，使用值类型就好了。&lt;/p&gt;
&lt;p&gt;值类型的接收器可以减少一定数量的内存垃圾生成，值类型接收器一般会在栈上分配到内存（但也不一定），在没搞明白代码想干什么之前，别为这个原因而选择值类型接收器。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;何时使用指针类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（1）如果方法需要修改接收器里的数据，则接收器必须是指针类型。
（2）如果接收器是一个包含了 sync.Mutex 或者类似同步字段的结构体，接收器必须是指针，这样可以避免拷贝。
（3）如果接收器是一个大的结构体或者数组，那么指针类型接收器更有效率。
（4）如果接收器是一个结构体，数组或者 slice，它们中任意一个元素是指针类型而且可能被修改，建议使用指针类型接收器，这样会增加程序的可读性。&lt;/p&gt;
&lt;p&gt;最后如果实在还是不知道该使用哪种接收器，那么记住使用指针接收器是最靠谱的。&lt;/p&gt;
&lt;h2&gt;20.4 匿名类型的方法提升&lt;/h2&gt;
&lt;p&gt;当一个匿名类型被嵌入在结构体中时，匿名类型的可见方法也同样被内嵌，这在效果上等同于外层类型继承了这些方法：将父类型放在子类型中来实现亚型。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果。&lt;/p&gt;
&lt;p&gt;当我们嵌入一个匿名类型，这个类型的方法就变成了外部类型的方法，但是当它的方法被调用时，方法的接收器是内部类型(嵌入的匿名类型)，而非外部类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type People struct {
	Age    int
	gender string
	Name   string
}

type OtherPeople struct {
	People
}

func (p People) PeInfo() {
	fmt.Println(&quot;People &quot;, p.Name, &quot;: &quot;, p.Age, &quot;岁, 性别:&quot;, p.gender)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此嵌入类型的名字充当着字段名，同时嵌入类型作为内部类型存在，我们可以使用下面的调用方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OtherPeople.People.PeInfo()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这儿我们可以通过类型名称来访问内部类型的字段和方法。然而，这些字段和方法也同样被提升到了外部类型，我们可以直接访问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OtherPeople.PeInfo()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前面我们看到了嵌入类型的方法提升，在 Go 语言中匿名嵌入类型方法集提升的规则：&lt;/p&gt;
&lt;p&gt;给定一个结构体类型 S 和一个命名为 T 的类型，方法提升像下面规定的这样被包含在结构体方法集中：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;简单地说是两条规则：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;规则一：如果S包含嵌入字段T，则S和*S的方法集都包括具有接收器T的提升方法。*S的方法集还包括具有接收器*T的提升方法。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;规则二：如果S包含嵌入字段*T，则S和*S的方法集都包括具有接收器T或*T的提升方法。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当嵌入一个类型，嵌入类型的接收器为指针的方法将不能被外部类型的值访问。这跟接口规则一致。&lt;/p&gt;
&lt;p&gt;注意：以上规则在调用指针方法 t.M() 时会被自动转换为 (&amp;amp;t).M() ，由于这个语法糖，导致我们很容易误解上面的规则不起作用，而实际上规则是有效的，在实际应用中我们可以留意这个问题。&lt;/p&gt;
&lt;p&gt;我们通过下面代码验证下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;reflect&quot;
)

type People struct {
	Age    int
	gender string
	Name   string
}

type OtherPeople struct {
	People
}

type NewPeople People

func (p *NewPeople) PeName(pname string) {
	fmt.Println(&quot;pold name:&quot;, p.Name)
	p.Name = pname
	fmt.Println(&quot;pnew name:&quot;, p.Name)
}

func (p NewPeople) PeInfo() {
	fmt.Println(&quot;NewPeople &quot;, p.Name, &quot;: &quot;, p.Age, &quot;岁, 性别:&quot;, p.gender)
}

func (p *People) PeName(pname string) {
	fmt.Println(&quot;old name:&quot;, p.Name)
	p.Name = pname
	fmt.Println(&quot;new name:&quot;, p.Name)
}

func (p People) PeInfo() {
	fmt.Println(&quot;People &quot;, p.Name, &quot;: &quot;, p.Age, &quot;岁, 性别:&quot;, p.gender)
}

func methodSet(a interface{}) {
	t := reflect.TypeOf(a)	
	fmt.Printf(&quot;%T\n&quot;, a)
	for i, n := 0, t.NumMethod(); i &amp;lt; n; i++ {
		m := t.Method(i)
		fmt.Println(i, &quot;:&quot;, m.Name, m.Type)
	}
}

func main() {
	p := OtherPeople{People{26, &quot;Male&quot;, &quot;张三&quot;\}\}
	p.PeInfo()
	p.PeName(&quot;Joke&quot;)

	methodSet(p) // T方法提升

	methodSet(&amp;amp;p) // *T和T方法提升

	pp := NewPeople{42, &quot;Male&quot;, &quot;李四&quot;}
	pp.PeInfo()
	pp.PeName(&quot;Haw&quot;)

	methodSet(&amp;amp;pp)
}


程序输出：
People  张三 :  26 岁, 性别: Male
old name: 张三
new name: Joke
main.OtherPeople
0 : PeInfo func(main.OtherPeople)
*main.OtherPeople
0 : PeInfo func(*main.OtherPeople)
1 : PeName func(*main.OtherPeople, string)
NewPeople  李四 :  42 岁, 性别: Male
pold name: 李四
pnew name: Haw
*main.NewPeople
0 : PeInfo func(*main.NewPeople)
1 : PeName func(*main.NewPeople, string)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以从上面输出看到，*OtherPeople 下有两个方法PeInfo(),PeName(string)可以调用，而OtherPeople只有一个方法PeInfo()可以调用。&lt;/p&gt;
&lt;p&gt;但是在Go中存在一个语法糖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	p.PeInfo()
	p.PeName(&quot;Joke&quot;)

	methodSet(p) // T方法提升
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然P 只有一个方法：PeInfo func(main.OtherPeople)，但我们依然可以调用p.PeName(&quot;Joke&quot;)。&lt;/p&gt;
&lt;p&gt;这里Go自动转为(&amp;amp;p).PeName(&quot;Joke&quot;)，其调用后结果让我们以为p有两个方法，其实这里p只有一个方法。&lt;/p&gt;
&lt;p&gt;有关于内嵌字段方法集的提升，初学者需要好好留意下这个规则。&lt;/p&gt;
&lt;p&gt;结合前面的自定义类型赋值接口类型的规则，与内嵌类型的方法集提升规则这两个大规则一定要弄清楚，只有彻底弄清楚这些规则，我们在阅读和写代码时才能做到气定神闲。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_19_interface.md&quot;&gt;第十九章 接口&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_21_goroutine.md&quot;&gt;第二十一章 协程(goroutine)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第十九章 接口</title><link>https://blog.wemang.com/posts/go/study/42_19_interface/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_19_interface/</guid><pubDate>Thu, 01 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;19.1 接口是什么&lt;/h2&gt;
&lt;p&gt;Go语言接口定义了一组方法集合，但是这些方法集合仅仅只是被定义，它们没有在接口中实现。接口(interface)类型是Go语言的一种数据类型。而因为所有的类型包括自定义类型都实现了空接口interface{}，所以空接口interface{}可以被当做任意类型的数值。&lt;/p&gt;
&lt;p&gt;Go 语言中的所有类型包括自定义类型都实现了interface{}接口，这意味着所有的类型如string、 int、 int64甚至是自定义的结构体类型都拥有interface{}空接口，这一点interface{}和Java中的Object类比较相似。&lt;/p&gt;
&lt;p&gt;接口类型的未初始化变量的值为nil。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var i interface{} = 99 // i可以是任何类型
i = 44.09
i = &quot;All&quot;  // i 可接受任意类型的赋值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接口是一组抽象方法的集合，它必须由其他非接口类型实现，不能自我实现。Go 语言通过它可以实现很多面向对象的特性。&lt;/p&gt;
&lt;p&gt;通过如下格式定义接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Namer interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的 Namer 是一个接口类型，按照惯例，单方法接口由方法名称加上-er后缀或类似修改来命名，以构造代理名词：Reader，Writer，Formatter，CloseNotifier等。还有一些不常用的方式（当后缀 er 不合适时），比如 Recoverable，此时接口名以 able 结尾，或者以 I 开头等。&lt;/p&gt;
&lt;p&gt;Go 语言中的接口都很简短，通常它们会包含 0 个、最多 3 个方法。如标准库io包中定义了下面2个接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Reader interface {
	Read(p []byte) (n int, err error)
}
type Writer interface {
	Write(p []byte) (n int, err error)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Go语言中，如果接口的所有方法在某个类型方法集中被实现，则认为该类型实现了这个接口。&lt;/p&gt;
&lt;p&gt;类型不用显式声明实现了接口，只需要实现接口所有方法，这样的隐式实现解藕了实现接口的包和定义接口的包。&lt;/p&gt;
&lt;p&gt;同一个接口可被多个类型可以实现，一个类型也可以实现多个接口。实现了某个接口的类型，还可以有其它的方法。有时我们甚至都不知道某个类型定义的方法集巧合地实现了某个接口。这种灵活性使我们不用像JAVA语言那样需要显式implement，一旦类型不需要实现某个接口，我们甚至可以不改动任何代码。&lt;/p&gt;
&lt;p&gt;类型需要实现接口方法集中的所有方法，一定是接口方法集中所有方法。类型实现了这个接口，那么接口类型的变量也就可以存放该类型的值。&lt;/p&gt;
&lt;p&gt;如下代码所示，结构体A和类型I都实现了接口B的方法f()，所有这两种类型也具有了接口B的一切特性，可以将该类型的值存储在接口B类型的变量中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type A struct {
	Books int
}

type B interface {
	f()
}

func (a A) f() {
	fmt.Println(&quot;A.f() &quot;, a.Books)
}

type I int

func (i I) f() {
	fmt.Println(&quot;I.f() &quot;, i)
}

func main() {
	var a A = A{Books: 9}
	a.f()

	var b B = A{Books: 99} // 接口类型可接受结构体A的值，因为结构体A实现了接口
	b.f()

	var i I = 199 // I是int类型引申出来的新类型
	i.f()

	var b2 B = I(299) // 接口类型可接受新类型I的值，因为新类型I实现了接口
	b2.f()
}

程序输出：
A.f()  9
A.f()  99
I.f()  199
I.f()  299
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果接口在类型之后才定义，或者二者处于不同的包中。但只要类型实现了接口中的所有方法，这个类型就实现了此接口。&lt;/p&gt;
&lt;p&gt;因此Go语言中接口具有强大的灵活性。&lt;/p&gt;
&lt;p&gt;注意：接口中的方法必须要全部实现，才能实现接口。&lt;/p&gt;
&lt;h2&gt;19.2 接口嵌入&lt;/h2&gt;
&lt;p&gt;一个接口可以包含一个或多个其他的接口，但是在接口内不能嵌入结构体，也不能嵌入接口自身，否则编译会出错。&lt;/p&gt;
&lt;p&gt;下面这两种嵌入接口自身的方式都不能编译通过:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 编译错误：invalid recursive type Bad
type Bad interface {
	Bad
}

// 编译错误：invalid recursive type Bad2
type Bad1 interface {
	Bad2
}
type Bad2 interface {
	Bad1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如下面的接口 File 包含了 ReadWrite 和 Lock 的所有方法，它还额外有一个 Close() 方法。接口的嵌入方式和结构体的嵌入方式语法上差不多，直接写接口名即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ReadWrite interface {
    Read(b Buffer) bool
    Write(b Buffer) bool
}

type Lock interface {
    Lock()
    Unlock()
}

type File interface {
    ReadWrite
    Lock
    Close()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;19.3 类型断言&lt;/h2&gt;
&lt;p&gt;前面我们可以把实现了某个接口的类型值保存在接口变量中，但反过来某个接口变量属于哪个类型呢？如何检测接口变量的类型呢？这就是类型断言（Type Assertion）的作用。&lt;/p&gt;
&lt;p&gt;接口类型I的变量 varI 中可以包含任何实现了这个接口的类型的值，如果多个类型都实现了这个接口，所以有时我们需要用一种动态方式来检测它的真实类型，即在运行时确定变量的实际类型。&lt;/p&gt;
&lt;p&gt;通常我们可以使用类型断言（value, ok := element.(T)）来测试在某个时刻接口变量 varI 是否包含类型 T 的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;value, ok := varI.(T)       // 类型断言
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;varI 必须是一个接口变量&lt;/strong&gt;，否则编译器会报错：invalid type assertion: varI.(T) (non-interface type (type of I) on left) 。&lt;/p&gt;
&lt;p&gt;类型断言可能是无效的，虽然编译器会尽力检查转换是否有效，但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var varI I
varI = T(&quot;Tstring&quot;)
if v, ok := varI.(T); ok { // 类型断言
	fmt.Println(&quot;varI类型断言结果为：&quot;, v) // varI已经转为T类型
	varI.f()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果断言成功，v 是 varI 转换到类型 T 的值，ok 会是 true；否则 v 是类型 T 的零值，ok 是 false，也没有运行时错误发生。&lt;/p&gt;
&lt;p&gt;接口类型向普通类型转换有两种方式：Comma-ok断言和Type-switch测试。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通过Type-switch做类型判断&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;接口变量的类型可以使用一种特殊形式的 switch 做类型断言：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Type-switch做类型判断
var value interface{}

switch str := value.(type) {
case string:
	fmt.Println(&quot;value类型断言结果为string:&quot;, str)

case Stringer:
	fmt.Println(&quot;value类型断言结果为Stringer:&quot;, str)

default:
	fmt.Println(&quot;value类型不在上述类型之中&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以用 Type-switch 进行运行时类型分析，但是在 type-switch 时不允许有 fallthrough 。Type-switch让我们在处理未知类型的数据时，比如解析 json 等编码的数据，会非常方便。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测试一个值是否实现了某个接口（Comma-ok断言）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们想测试它是否实现了 I 接口，可以这样做类型断言：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Comma-ok断言
var varI I
varI = T(&quot;Tstring&quot;)
if v, ok := varI.(T); ok { // 类型断言
	fmt.Println(&quot;varI类型断言结果为：&quot;, v) // varI已经转为T类型
	varI.f()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接口描述了一系列的行为，规定可以做什么行为，“当一个东西，走起来像鸭子，叫起来也像鸭子，游泳也像鸭子，那么我们可以认为他就是一只鸭子”。类型实现不同的接口将拥有不同的行为方法集合，这就是多态的本质。&lt;/p&gt;
&lt;p&gt;下面是上面几个代码片段的完整代码文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type I interface {
	f()
}

type T string

func (t T) f() {
	fmt.Println(&quot;T Method&quot;)
}

type Stringer interface {
	String() string
}

func main() {

	// 类型断言
	var varI I
	varI = T(&quot;Tstring&quot;)
	if v, ok := varI.(T); ok { // 类型断言
		fmt.Println(&quot;varI类型断言结果为：&quot;, v) // varI已经转为T类型
		varI.f()
	}

	// Type-switch做类型判断
	var value interface{} // 默认为零值

	switch str := value.(type) {
	case string:
		fmt.Println(&quot;value类型断言结果为string:&quot;, str)

	case Stringer:
		fmt.Println(&quot;value类型断言结果为Stringer:&quot;, str)

	default:
		fmt.Println(&quot;value类型不在上述类型之中&quot;)
	}

	// Comma-ok断言
	value = &quot;类型断言检查&quot;
	str, ok := value.(string)
	if ok {
		fmt.Printf(&quot;value类型断言结果为：%T\n&quot;, str) // str已经转为string类型
	} else {
		fmt.Printf(&quot;value不是string类型 \n&quot;)
	}
}

程序输出：
varI类型断言结果为： Tstring
T Method
value类型不在上述类型之中
value类型断言结果为：string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用接口使代码更具有普适性，例如函数的参数为接口变量。标准库中遵循了这个原则，但如果对接口概念没有良好的把握，是不能很好理解它是如何构建的。&lt;/p&gt;
&lt;p&gt;那么为什么在Go语言中我们可以进行类型断言呢？我们可以在上面代码中看到，断言后的值 v, ok := varI.(T)，v值对应的是一个类型名：Tstring 。 因为在Go语言中，一个接口值(Interface Value)其实是由两部分组成：type :value 。所以在做类型断言时，变量只能是接口类型变量，断言得到的值其实是接口值中对应的类型名。这在后面讨论reflect反射包时将会有更深入的说明。&lt;/p&gt;
&lt;h2&gt;19.4 接口与动态类型&lt;/h2&gt;
&lt;p&gt;在经典的面向对象语言（像 C++，Java 和 C#）中，往往将数据和方法被封装为类的概念：类中包含它们两者，并且不能剥离。&lt;/p&gt;
&lt;p&gt;Go 语言中没有类，数据（结构体或更一般的类型）和方法是一种松耦合的正交关系。Go 语言中的接口必须提供一个指定方法集的实现，但是更加灵活通用：任何提供了接口方法实现代码的类型都隐式地实现了该接口，而不用显式地声明。该特性允许我们在不改变已有的代码的情况下定义和使用新接口。&lt;/p&gt;
&lt;p&gt;接收一个（或多个）接口类型作为参数的函数，其实参可以是任何实现了该接口的类型。 实现了某个接口的类型可以被传给任何以此接口为参数的函数 。&lt;/p&gt;
&lt;p&gt;Go 语言动态类型的实现通常需要编译器静态检查的支持：当变量被赋值给一个接口类型的变量时，编译器会检查其是否实现了该接口的所有方法。我们也可以通过类型断言来检查接口变量是否实现了相应类型。&lt;/p&gt;
&lt;p&gt;因此 Go 语言提供了动态语言的优点，却没有其他动态语言在运行时可能发生错误的缺点。Go 语言的接口提高了代码的分离度，改善了代码的复用性，使得代码开发过程中的设计模式更容易实现。&lt;/p&gt;
&lt;h2&gt;19.5 接口的提取&lt;/h2&gt;
&lt;p&gt;接口的提取，是非常有用的设计模式，良好的提取可以减少需要的类型和方法数量。而且在Go语言中不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。&lt;/p&gt;
&lt;p&gt;假设有一些拥有共同行为的对象，并且开发者想要抽象出这些行为，这时就可以创建一个接口来使用。在Go语言中这样操作甚至不会影响到前面开发的代码，所以我们不用提前设计出所有的接口，接口的设计可以不断演进，并且不用废弃之前的决定。而且类型要实现某个接口，类型本身不用改变，只需要在这个类型上实现新的接口方法集。&lt;/p&gt;
&lt;h2&gt;19.6 接口的继承&lt;/h2&gt;
&lt;p&gt;当一个类型包含（内嵌）另一个类型（实现了一个或多个接口）时，这个类型就可以使用（另一个类型）所有的接口方法。&lt;/p&gt;
&lt;p&gt;类型可以通过继承多个接口来提供像多重继承一样的特性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ReaderWriter struct {
    io.Reader
    io.Writer
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_18_struct.md&quot;&gt;第十八章 Struct 结构体&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_20_method.md&quot;&gt;第二十章 方法&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第十八章 Struct 结构体</title><link>https://blog.wemang.com/posts/go/study/42_18_struct/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_18_struct/</guid><pubDate>Wed, 31 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;18.1结构体(struct)&lt;/h2&gt;
&lt;p&gt;Go 通过结构体的形式支持用户自定义类型，或者叫定制类型。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Go 语言结构体是实现自定义类型的一种重要数据类型。&lt;/p&gt;
&lt;p&gt;结构体是复合类型（composite types），它由一系列属性组成，每个属性都有自己的类型和值的，结构体通过属性把数据聚集在一起。&lt;/p&gt;
&lt;p&gt;结构体类型和字段的命名遵循可见性规则。&lt;/p&gt;
&lt;p&gt;方法（Method）可以访问这些数据，就好像它们是这个独立实体的一部分。&lt;/p&gt;
&lt;p&gt;结构体是值类型，因此可以通过 new 函数来创建。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;结构体是由一系列称为字段（fields）的命名元素组成，每个元素都有一个名称和一个类型。 字段名称可以显式指定（IdentifierList）或隐式指定（EmbeddedField），没有显式字段名称的字段称为匿名（内嵌）字段。在结构体中，非空字段名称必须是唯一的。&lt;/p&gt;
&lt;p&gt;结构体定义的一般方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type identifier struct {
    field1 type1
    field2 type2
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结构体里的字段一般都有名字，像 field1、field2 等，如果字段在代码中从来也不会被用到，那么可以命名它为 _。&lt;/p&gt;
&lt;p&gt;空结构体如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具有6个字段的结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct {
	x, y int
	u float32
	_ float32  // 填充
	A *[]int
	F func()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于匿名字段，必须将匿名字段指定为类型名称T或指向非接口类型名称* T的指针，并且T本身可能不是指针类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct {
	T1        // 字段名 T1
	*T2       // 字段名 T2
	P.T3      // 字段名 T3
	*P.T4     // f字段名T4
	x, y int    // 字段名 x 和 y
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 new 函数给一个新的结构体变量分配内存，它返回指向已分配内存的指针：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type S struct { a int; b float64 }
new(S)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;new(S)为S类型的变量分配内存，并初始化（a = 0，b = 0.0），返回包含该位置地址的类型* S的值。&lt;/p&gt;
&lt;p&gt;我们一般的惯用方法是：t := new(T)，变量 t 是一个指向 T的指针，此时结构体字段的值是它们所属类型的零值。&lt;/p&gt;
&lt;p&gt;也可以这样写：var t T ，也会给 t 分配内存，并零值化内存，但是这个时候 t 是类型T。&lt;/p&gt;
&lt;p&gt;在这两种方式中，t 通常被称做类型 T 的一个实例（instance）或对象（object）。&lt;/p&gt;
&lt;p&gt;使用点号符“.”可以获取结构体字段的值structname.fieldname。无论变量是一个结构体类型还是一个结构体类型指针，都使用同样的表示法来引用结构体的字段。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type myStruct struct { i int }
var v myStruct    // v是结构体类型变量
var p *myStruct   // p是指向一个结构体类型变量的指针
v.i
p.i

type Interval struct {
    start  int
    end   int
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结构体变量有下面几种初始化方式，前面一种按照字段顺序，后面两种则对应字段名来初始化赋值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
intr := Interval{0, 3}            (A)
intr := Interval{end:5, start:1}    (B)
intr := Interval{end:5}           (C)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;复合字面量是构造结构体，数组，切片和字典的值，并每次都创建新值。声明和初始化一个结构体实例（一个结构体字面量：struct-literal）方式如下：&lt;/p&gt;
&lt;p&gt;定义结构体类型Point3D和Line：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Point3D struct { x, y, z float64 }
type Line struct { p, q Point3D }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;声明并初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;origin := Point3D{}                      //  Point3D 是零值
line := Line{origin, Point3D{y: -4, z: 12.3\}\}  //   line.q.x 是零值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 Point3D{}以及 &lt;code&gt;Line{origin, Point3D{y: -4, z: 12.3\}\}&lt;/code&gt;都是结构体字面量。&lt;/p&gt;
&lt;p&gt;表达式 new(Type) 和 &amp;amp;Type{} 是等价的。&lt;code&gt;&amp;amp;struct1{a, b, c}&lt;/code&gt; 是一种简写，底层仍然会调用 new ()，这里值的顺序必须按照字段顺序来写。也可以通过在值的前面放上字段名来初始化字段的方式，这种方式就不必按照顺序来写了。&lt;/p&gt;
&lt;p&gt;结构体类型和字段的命名遵循可见性规则，一个导出的结构体类型中有些字段是导出的，也即首字母大写字段会导出；另一些不可见，也即首字母小写为未导出，对外不可见。&lt;/p&gt;
&lt;h2&gt;18.2 结构体特性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;结构体的内存布局&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go 语言中，结构体和它所包含的数据在内存中是以连续块的形式存在的，即使结构体中嵌套有其他的结构体，这在性能上带来了很大的优势。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;递归结构体&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;递归结构体类型可以通过引用自身指针来定义。这在定义链表或二叉树的节点时特别有用，此时节点包含指向临近节点的链接。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Element struct {
	// Next and previous pointers in the doubly-linked list of elements.
	// To simplify the implementation, internally a list l is implemented
	// as a ring, such that &amp;amp;l.root is both the next element of the last
	// list element (l.Back()) and the previous element of the first list
	// element (l.Front()).
	next, prev *Element

	// The list to which this element belongs.
	list *List

	// The value stored with this element.
	Value interface{}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;可见性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过参考应用可见性规则，如果结构体名不能导出，可使用 new 函数使用工厂方法的方法达到同样的目的。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type bitmap struct {
	Size int
	data []byte
}

func NewBitmap(size int) *bitmap {
	div, mod := size/8, size%8
	if mod &amp;gt; 0 {
		div++
	}
	return &amp;amp;bitmap{size, make([]byte, div)}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在包外，只有通过NewBitmap函数才可以初始bitmap结构体。同理，在bitmap结构体中，由于其字段data是小写字母开头即并未导出，bitmap结构体的变量不能直接通过选择器读取data字段的数据。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;带标签的结构体&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结构体中的字段除了有名字和类型外，还可以有一个可选的标签（tag）。它是一个附属于字段的字符串，可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用，只有 reflect 包能获取它。&lt;/p&gt;
&lt;p&gt;reflect包可以在运行时反射得到类型、属性和方法。如变量是结构体类型，可以通过 Field() 方法来索引结构体的字段，然后就可以得到Tag 属性。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;reflect&quot;
)

type Student struct {
	name string &quot;学生名字&quot;          // 结构体标签
	Age  int    &quot;学生年龄&quot;          // 结构体标签
	Room int    `json:&quot;Roomid&quot;` // 结构体标签
}

func main() {
	st := Student{&quot;Titan&quot;, 14, 102}
	fmt.Println(reflect.TypeOf(st).Field(0).Tag)
	fmt.Println(reflect.TypeOf(st).Field(1).Tag)
	fmt.Println(reflect.TypeOf(st).Field(2).Tag)
}

程序输出：
学生名字
学生年龄
json:&quot;Roomid&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面代码中可以看到，通过reflect我们很容易得到结构体字段的标签。&lt;/p&gt;
&lt;h2&gt;18.3 匿名成员&lt;/h2&gt;
&lt;p&gt;Go语言结构体中可以包含一个或多个匿名（内嵌）字段，即这些字段没有显式的名字，只有字段的类型是必须的，此时类型就是字段的名字（这一特征决定了在一个结构体中，每种数据类型只能有一个匿名字段）。&lt;/p&gt;
&lt;p&gt;匿名（内嵌）字段本身也可以是一个结构体类型，即结构体可以包含内嵌结构体。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Human struct {
	name string
}

type Student struct { // 含内嵌结构体Human
	Human // 匿名（内嵌）字段
	int   // 匿名（内嵌）字段
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go语言结构体中这种含匿名（内嵌）字段和内嵌结构体的结构，可近似地理解为面向对象语言中的继承概念。&lt;/p&gt;
&lt;p&gt;Go 语言中的继承是通过内嵌或者说组合来实现的，所以可以说，在 Go 语言中，相比较于继承，组合更受青睐。&lt;/p&gt;
&lt;h2&gt;18.4 嵌入与聚合&lt;/h2&gt;
&lt;p&gt;结构体中包含匿名（内嵌）字段叫嵌入或者内嵌；而如果结构体中字段包含了类型名，还有字段名，则是聚合。聚合的在JAVA和C++都是常见的方式，而内嵌则是Go 的特有方式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Human struct {
	name string
}

type Person1 struct {           // 内嵌
	Human
}

type Person2 struct {           // 内嵌， 这种内嵌与上面内嵌有差异
	*Human
}

type Person3 struct{             // 聚合
	human Human
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;嵌入在结构体中广泛使用，在Go语言中如果只考虑结构体和接口的嵌入组合方式，一共有下面四种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.在接口中嵌入接口:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里指的是在接口中定义中嵌入接口类型，而不是接口的一个实例，相当于合并了两个接口类型定义的全部函数。下面只有同时实现了Writer和 Reader 的接口，才可以说是实现了Teacher接口，即可以作为Teacher的实例。Teacher接口嵌入了Writer和 Reader 两个接口，在Teacher接口中，Writer和 Reader是两个匿名（内嵌）字段。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Writer interface{
   Write()
}

type Reader interface{
   Read()
} 

type Teacher interface{
  Reader
  Writer
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;2.在接口中嵌入结构体:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种方式在Go语言中是不合法的，不能通过编译。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Human struct {
	name string
}

type Writer interface {
	Write()
}

type Reader interface {
	Read()
}

type Teacher interface {
	Reader
	Writer
	Human
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;存在语法错误，并不具有实际的含义，编译报错:
interface contains embedded non-interface Base&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Interface 不能嵌入非interface的类型。
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;3.在结构体中内嵌接口:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;初始化的时候，内嵌接口要用一个实现此接口的结构体赋值；或者定义一个新结构体，可以把新结构体作为receiver，实现接口的方法就实现了接口（先记住这句话，后面在讲述方法时会解释），这个新结构体可作为初始化时实现了内嵌接口的结构体来赋值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type Writer interface {
	Write()
}

type Author struct {
	name string
	Writer
}

// 定义新结构体，重点是实现接口方法Write()
type Other struct {
	i int
}

func (a Author) Write() {
	fmt.Println(a.name, &quot;  Write.&quot;)
}

// 新结构体Other实现接口方法Write()，也就可以初始化时赋值给Writer 接口
func (o Other) Write() {
	fmt.Println(&quot; Other Write.&quot;)
}

func main() {

	//  方法一：Other{99}作为Writer 接口赋值
	Ao := Author{&quot;Other&quot;, Other{99\}\}
	Ao.Write()

	// 方法二：简易做法，对接口使用零值，可以完成初始化
	Au := Author{name: &quot;Hawking&quot;}
	Au.Write()
}

程序输出：
Other   Write.
Hawking   Write.
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;4.在结构体中嵌入结构体:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在结构体嵌入结构体很好理解，但不能嵌入自身值类型，可以嵌入自身的指针类型即递归嵌套。&lt;/p&gt;
&lt;p&gt;在初始化时，内嵌结构体也进行赋值；外层结构自动获得内嵌结构体所有定义的字段和实现的方法。&lt;/p&gt;
&lt;p&gt;下面代码完整演示了结构体中嵌入结构体，初始化以及字段的选择调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type Human struct {
	name   string // 姓名
	Gender string // 性别
	Age    int    // 年龄
	string        // 匿名字段
}

type Student struct {
	Human     // 匿名字段
	Room  int // 教室
	int       // 匿名字段
}

func main() {
	//使用new方式
	stu := new(Student)
	stu.Room = 102
	stu.Human.name = &quot;Titan&quot;
	stu.Gender = &quot;男&quot;
	stu.Human.Age = 14
	stu.Human.string = &quot;Student&quot;

	fmt.Println(&quot;stu is:&quot;, stu)
	fmt.Printf(&quot;Student.Room is: %d\n&quot;, stu.Room)
	fmt.Printf(&quot;Student.int is: %d\n&quot;, stu.int) // 初始化时已自动给予零值：0
	fmt.Printf(&quot;Student.Human.name is: %s\n&quot;, stu.name) //  (*stu).name
	fmt.Printf(&quot;Student.Human.Gender is: %s\n&quot;, stu.Gender)
	fmt.Printf(&quot;Student.Human.Age is: %d\n&quot;, stu.Age)
	fmt.Printf(&quot;Student.Human.string is: %s\n&quot;, stu.string)

	// 使用结构体字面量赋值
	stud := Student{Room: 102, Human: Human{&quot;Hawking&quot;, &quot;男&quot;, 14, &quot;Monitor&quot;\}\}

	fmt.Println(&quot;stud is:&quot;, stud)
	fmt.Printf(&quot;Student.Room is: %d\n&quot;, stud.Room)
	fmt.Printf(&quot;Student.int is: %d\n&quot;, stud.int) // 初始化时已自动给予零值：0
	fmt.Printf(&quot;Student.Human.name is: %s\n&quot;, stud.Human.name)
	fmt.Printf(&quot;Student.Human.Gender is: %s\n&quot;, stud.Human.Gender)
	fmt.Printf(&quot;Student.Human.Age is: %d\n&quot;, stud.Human.Age)
	fmt.Printf(&quot;Student.Human.string is: %s\n&quot;, stud.Human.string)
}

程序输出：
stu is: &amp;amp;{ {Titan 男 14 Student} 102 0}
Student.Room is: 102
Student.int is: 0
Student.Human.name is: Titan
Student.Human.Gender is: 男
Student.Human.Age is: 14
Student.Human.string is: Student
stud is: { {Hawking 男 14 Monitor} 102 0}
Student.Room is: 102
Student.int is: 0
Student.Human.name is: Hawking
Student.Human.Gender is: 男
Student.Human.Age is: 14
Student.Human.string is: Monitor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内嵌结构体的字段，可以逐层选择来使用，如stu.Human.name。如果外层结构体中没有同名的name字段，也可以直接选择使用，如stu.name。&lt;/p&gt;
&lt;p&gt;通过对结构体使用&lt;code&gt;new(T)，struct{filed:value}&lt;/code&gt;两种方式来声明初始化，分别可以得到*T指针变量，和T值变量。&lt;/p&gt;
&lt;p&gt;从上面程序输出结果中&lt;code&gt;stu is: &amp;amp;{ {Titan 男 14 Student} 102 0}&lt;/code&gt; 可以得知，stu 是指针变量。但是程序在调用此结构体变量的字段时并没有使用到指针，这是因为这里的 stu.name  相当于(*stu).name，这是一个语法糖，一般都使用stu.name方式来调用，但要知道有这个语法糖存在。&lt;/p&gt;
&lt;h2&gt;18.5 命名冲突&lt;/h2&gt;
&lt;p&gt;当结构体两个字段拥有相同的名字（可能是继承来的名字）时会怎么样呢？外层名字会覆盖内层名字（但是两者的内存空间都保留）。&lt;/p&gt;
&lt;p&gt;当相同的字段名在同一层级出现了两次，而且这个名字被程序直接选择使用了，就会引发一个错误，可以采用逐级选择使用的方式来避免这个错误。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type A struct {a int}
type B struct {a int}

type C struct {
A
B
}
var c C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中不能直接选择使用c.a，编译时会报告ambiguous selector c.a，且编译不能通过。但是完整逐级写出来就正常了，例如c.A.a或者c.B.a 都可以正确得到对应的值。&lt;/p&gt;
&lt;p&gt;解决直接选择使用c.a引发二义性的问题一般应该由程序员逐级完整写出避免错误。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_17_type.md&quot;&gt;第十七章 Type关键字&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_19_interface.md&quot;&gt;第十九章 接口&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第十七章 type关键字</title><link>https://blog.wemang.com/posts/go/study/42_17_type/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_17_type/</guid><pubDate>Tue, 30 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;type关键字在Go语言中作用很重要，比如定义结构体，接口，还可以自定义类型，定义类型别名等。自定义类型由一组值以及作用于这些值的方法组成，类型一般有类型名称，往往从现有类型组合通过type关键字构造出一个新的类型。&lt;/p&gt;
&lt;h2&gt;17.1 type 自定义类型&lt;/h2&gt;
&lt;p&gt;在Go 语言中，基础类型有下面几种：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    bool byte complex64 complex128 error float32 float64
    int int8 int16 int32 int64 rune string
    uint uint8 uint16 uint32 uint64 uintptr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 type 关键字可以定义我们自己的类型，如我们可以使用type定义一个新的结构体，但也可以把一个已经存在的类型作为基础类型而定义新类型，然后就可以在我们的代码中使用新的类型名字，这称为自定义类型，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type IZ int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里IZ就是完全是一种新类型，然后我们可以使用下面的方式声明变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a IZ = 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们可以看到 int 是变量 a 的底层类型，这也使得它们之间存在相互转换的可能。&lt;/p&gt;
&lt;p&gt;如果我们有多个类型需要定义，可以使用因式分解关键字的方式，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type (
   IZ int
   FZ float64
   STR string
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 type IZ int 中，IZ 就是在 int 类型基础构建的新名称，这称为自定义类型。然后就可以使用 IZ 来操作 int 类型的数据。使用这种方法定义之后的类型可以拥有更多的特性，但是在类型转换时必须显式转换。&lt;/p&gt;
&lt;p&gt;每个值都必须在经过编译后属于某个类型（编译器必须能够推断出所有值的类型），因为 Go 语言是一种静态类型语言。在必要以及可行的情况下，一个类型的值可以被转换成另一种类型的值。由于 Go 语言不存在隐式类型转换，因此所有的转换都必须显式说明，就像调用一个函数一样（类型在这里的作用可以看作是一种函数）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;valueOfTypeB = typeB(valueOfTypeA)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类型 B 的值 = 类型 B(类型 A 的值)&lt;/p&gt;
&lt;p&gt;type TZ int 中，&lt;strong&gt;新类型不会拥有原基础类型所附带的方法&lt;/strong&gt;，如下面代码所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type A struct {
	Face int
}
type Aa A // 自定义新类型Aa，没有基础类型A的方法

func (a A) f() {
	fmt.Println(&quot;hi &quot;, a.Face)
}

func main() {
	var s A = A{ Face: 9 }
	s.f()

	var sa Aa = Aa{ Face: 9 }
	sa.f()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;编译错误信息：sa.f undefined (type Aa has no field or method f)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过type 关键字在原有类型基础上构造出一个新类型，我们需要针对新类型来重新创建新方法。&lt;/p&gt;
&lt;h2&gt;17.2 type 定义类型别名&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type IZ = int 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种写法其实是定义了int类型的别名，类型别名在1.9中实现，可将别名类型和原类型这两个类型视为完全一致使用。type IZ int 其实是定义了新类型，这和类型别名完全不是一个含义。自定义类型不会拥有原类型附带的方法，而别名是拥有原类型附带的。下面举2个例子说明：&lt;/p&gt;
&lt;p&gt;如果是类型别名，完整拥有其方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
package main

import (
	&quot;fmt&quot;
)

type A struct {
	Face int
}
type Aa=A // 类型别名

func (a A) f() {
	fmt.Println(&quot;hi &quot;, a.Face)
}

func main() {
	var s A = A{Face: 9}
	s.f()

	var sa Aa = Aa{Face: 9}
	sa.f()
}


程序输出：
hi  9
hi  9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结构化的类型没有真正的值，它使用 nil 作为默认值（在 Objective-C 中是 nil，在 Java 中是 null，在 C 和 C++ 中是NULL或 0）。值得注意的是，Go 语言中不存在类型继承。&lt;/p&gt;
&lt;p&gt;函数也是一个确定的类型，就是以函数签名作为类型。这种类型的定义例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type  typeFunc func ( int, int) int 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以在函数体中的某处返回使用类型为 typeFunc 的变量 varfunc：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return varfunc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义类型不会继承原有类型的方法，但接口方法或组合类型的内嵌元素则保留原有的方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//  Mutex 用两种方法，Lock and Unlock。
type Mutex struct         { /* Mutex fields */ }
func (m *Mutex) Lock()    { /* Lock implementation */ }
func (m *Mutex) Unlock()  { /* Unlock implementation */ }

// NewMutex和 Mutex 一样的数据结构，但是其方法是空的。
type NewMutex Mutex

// PtrMutex 的方法也是空的
type PtrMutex *Mutex

// *PrintableMutex 拥有Lock and Unlock 方法
type PrintableMutex struct {
    Mutex
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_16_function.md&quot;&gt;第十六章 函数&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_18_struct.md&quot;&gt;第十八章 Struct 结构体&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第十六章 函数</title><link>https://blog.wemang.com/posts/go/study/42_16_function/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_16_function/</guid><pubDate>Mon, 29 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;16.1 函数介绍&lt;/h2&gt;
&lt;p&gt;Go语言函数基本组成：关键字func、函数名、参数列表、返回值、函数体和返回语句。语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func 函数名(参数列表) (返回值列表) {
    // 函数体
return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了main()、init()函数外，其它所有类型的函数都可以有参数与返回值。&lt;/p&gt;
&lt;p&gt;对于函数，一般也可以这么认为：&quot;func&quot; FunctionName Signature [ FunctionBody ] .&lt;/p&gt;
&lt;p&gt;&quot;func&quot; 为定义函数的关键字，FunctionName 为函数名，Signature 为函数签名，FunctionBody 为函数体。以下面定义的函数为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func FunctionName (a typea, b typeb) (t1 type1, t2 type2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数签名由函数参数、返回值以及它们的类型组成，被统称为函数签名。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(a typea, b typeb) (t1 type1, t2 type2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果两个函数的参数列表和返回值列表的变量类型能一一对应，那么这两个函数就有相同的签名，下面testa与testb具有相同的函数签名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func testa  (a, b int, z float32) bool
func testb  (a, b int, z float32) (bool)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数调用传入的参数必须按照参数声明的顺序。而且Go语言没有默认参数值的说法。函数签名中的最后传入参数可以具有前缀为....的类型（...int），这样的参数称为可变参数，并且可以使用零个或多个参数来调用该函数，这样的函数称为变参函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func doFix (prefix string, values ...int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数的参数和返回值列表始终带括号，但如果只有一个未命名的返回值（且只有此种情况），则可以将其写为未加括号的类型；一个函数也可以拥有多返回值，返回类型之间需要使用逗号分割，并使用小括号 () 将它们括起来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func testa  (a, b int, z float32) bool
func swap  (a int, b int) (t1 int, t2 int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在函数体中，参数是局部变量，被初始化为调用者传入的值。函数的参数和具名返回值是函数最外层的局部变量，它们的作用域就是整个函数。如果函数的签名声明了返回值，则函数体的语句列表必须以终止语句结束。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func IndexRune(s string, r rune) int {
	for i, c := range s {
		if c == r {
			return i
		}
	}
	return // 必须要有终止语句，如果这里没有return，则会编译错误：missing return at end of function
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数重载（function overloading）指的是可以编写多个同名函数，只要它们拥有不同的形参或者不同的返回值，在 Go 语言里面函数重载是不被允许的。&lt;/p&gt;
&lt;p&gt;函数也可以作为函数类型被使用。函数类型也就是函数签名，函数类型表示具有相同参数和结果类型的所有函数的集合。函数类型的未初始化变量的值为nil。就像下面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type  funcType func (int, int) int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面通过type关键字，定义了一个新类型，函数类型 funcType 。&lt;/p&gt;
&lt;p&gt;函数也可以在表达式中赋值给变量，这样作为表达式中右值出现，我们称之为函数值字面量（function literal），函数值字面量是一种表达式，它的值被称为匿名函数，就像下面一样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;f := func() int { return 7 }  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面代码对以上2种情况都做了定义和调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
package main

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

type funcType func(time.Time)     // 定义函数类型funcType

func main() {
	f := func(t time.Time) time.Time { return t } // 方式一：直接赋值给变量
	fmt.Println(f(time.Now()))

	var timer funcType = CurrentTime // 方式二：定义函数类型funcType变量timer
	timer(time.Now())

	funcType(CurrentTime)(time.Now())  // 先把CurrentTime函数转为funcType类型，然后传入参数调用
// 这种处理方式在Go 中比较常见
}

func CurrentTime(start time.Time) {
	fmt.Println(start)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;16.2 函数调用&lt;/h2&gt;
&lt;p&gt;Go 语言中函数默认使用按值传递来传递参数，也就是传递参数的副本。函数接收参数副本之后，在使用变量的过程中可能对副本的值进行更改，但不会影响到原来的变量。&lt;/p&gt;
&lt;p&gt;如果我们希望函数可以直接修改参数的值，而不是对参数的副本进行操作，则需要将参数的地址传递给函数，这就是按引用传递，比如 Function(&amp;amp;arg1)，此时传递给函数的是一个指针。如果传递给函数的是一个指针，我们可以通过这个指针来修改对应地址上的变量值。&lt;/p&gt;
&lt;p&gt;在函数调用时，像切片（slice）、字典（map）、接口（interface）、通道（channel）等这样的引用类型都是默认使用引用传递。&lt;/p&gt;
&lt;p&gt;命名返回值被初始化为相应类型的零值，当需要返回的时候，我们只需要一条简单的不带参数的return语句。需要注意的是，即使只有一个命名返回值，也需要使用 () 括起来&lt;/p&gt;
&lt;p&gt;前面说过，函数签名中的最后传入参数可以具有前缀为....的类型（...int），这样的函数称为变参函数。&lt;/p&gt;
&lt;p&gt;变参函数可以接受某种类型的切片 slice 为参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
package main

import (
	&quot;fmt&quot;
)

// 变参函数，参数不定长
func list(nums ...int) {
	fmt.Println(nums)
}

func main() {
	// 常规调用，参数可以多个
	list(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

	// 在参数同类型时，可以组成slice使用 parms... 进行参数传递
	numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	list(numbers...) // slice时使用
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;16.3 内置函数&lt;/h2&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;close&lt;/td&gt;
&lt;td&gt;用于通道，对于通道c，内置函数close(c)将不再在通道c上发送值。 如果c是仅接收通道，则会出错。 发送或关闭已关闭的通道会导致运行时错误。 关闭nil通道也会导致运行时错误。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;new、make&lt;/td&gt;
&lt;td&gt;new 和 make 均是用于分配内存：new用于值类型的内存分配，并且置为零值。make只用于slice、map以及channel这三种引用数据类型的内存分配和初始化。new(T) 分配类型 T 的零值并返回其地址，也就是指向类型 T 的指针。make(T) 它返回类型T的值（不是* T）。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;make()内置函数声明不同类型时的参数以及具体作用请见下面说明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;调用           T的类型     结果

make(T, n)       slice        T为切片类型，长度和容量都为n
make(T, n, m)     slice        T为切片类型，长度为n，容量为m （n&amp;lt;=m ，否则错误）

make(T)          map        T为字典类型
make(T, n)        map        T为字典类型，初始化n个元素的空间

make(T)          channel      T为通道类型，无缓冲区
make(T, n)        channel      T为通道类型，缓冲区长度为n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;make()内置函数的实际使用举例见下面代码以及注释：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s := make([]int, 10, 100)       // slice with len(s) == 10, cap(s) == 100
s := make([]int, 1e3)           // slice with len(s) == cap(s) == 1000
s := make([]int, 1&amp;lt;&amp;lt;63)         // illegal: len(s) is not representable by a value of type int
s := make([]int, 10, 0)         // illegal: len(s) &amp;gt; cap(s)
c := make(chan int, 10)         // channel with a buffer size of 10
m := make(map[string]int, 100)  // map with initial space for approximately 100 elements
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;new(T)内置函数在运行时为该类型的变量分配内存，并返回指向它的类型* T的值。 并对变量初始化。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type S struct { a int; b float64 }
new(S)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;new(S)为S类型的变量分配内存，并初始化（a = 0，b = 0.0），返回包含该位置地址的类型* S的值。&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;len(s)&lt;/td&gt;
&lt;td&gt;string type ，[n]T, *[n]T ，[]T ，map[K]T ，chan T&lt;/td&gt;
&lt;td&gt;string的长度（按照字节计算），数组长度 ，切片长度 ，字典长度 ，通道缓冲区中排队的元素数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cap(s)&lt;/td&gt;
&lt;td&gt;[n]T, *[n]T ，[]T ，chan T&lt;/td&gt;
&lt;td&gt;数组长度 ，切片容量 ，通道缓冲区容量&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对于len(s)和cap(s)，如果s为nil值，则两个函数的取值都是0，我们还需要记住一个规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 &amp;lt;= len(s) &amp;lt;= cap(s)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Go语言中，常量在某些计算条件下也可以通过表达式计算得到。比如：如果s是字符串常量，则表达式len(s)是常量。 如果s的类型是数组或指向数组的指针而表达式不包含通道接收或（非常量）函数调用，则表达式len(s)和cap(s)是常量；否则len和cap的调用不是常量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const (
	c1 = imag(2i)                  // imag(2i) = 2.0 是常量
	c2 = len([10]float64{2})         // [10]float64{2} 无函数调用
	c3 = len([10]float64{c1})        // [10]float64{c1} 无函数调用
	c4 = len([10]float64{imag(2i)})   // imag(2i)常量无函数调用
	c5 = len([10]float64{imag(z)})    // 无效: imag(z) 非常量函数调用
)
var z complex128
&lt;/code&gt;&lt;/pre&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;append&lt;/td&gt;
&lt;td&gt;用于附加连接切片&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;copy&lt;/td&gt;
&lt;td&gt;用于复制切片&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;delete&lt;/td&gt;
&lt;td&gt;从字典删除元素&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;append(s S, x ...T) S  // T 是类型S的元素
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;append内置函数是变参函数，常常用来附加切片元素，将零或多个值x附加到S类型的切片s，它的可变参数必须是切片类型，并返回结果切片，也就是是S类型。值x传递给类型为...的参数T，其中T 是S的元素类型，并且适用相应的参数传递规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s0 := []int{0, 0}
s1 := append(s0, 2)            // append 附加连接单个元素   s1 == []int{0, 0, 2}
s2 := append(s1, 3, 5, 7)        // append 附加连接多个元素  s2 == []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...)         // append 附加连接切片s0  s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...)  // append 附加切片指定值 s4 == []int{3, 5, 7, 2, 3, 5, 7, 0, 0}

var t []interface{}
t = append(t, 42, 3.1415, &quot;foo&quot;)  //  t == []interface{}{42, 3.1415, &quot;foo&quot;}

var b []byte
b = append(b, &quot;bar&quot;...)         // append 附加连接字符串内容  b == []byte{&apos;b&apos;, &apos;a&apos;, &apos;r&apos; }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;copy(dst, src []T) int
copy(dst []byte, src string) int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;copy内置函数常常将切片元素从源src复制到目标dst，并返回复制的元素数。 两个参数必须具有相同的元素类型T，并且必须可以分配给类型为[] T的切片。 复制的元素数量是len（src）和len（dst）的最小值。&lt;/p&gt;
&lt;p&gt;作为特殊情况，copy函数还接受可分配给[] byte类型的目标参数，其中source参数为字符串类型。 此种情况将字符串中的字节复制到字节切片中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int, 6)
var b = make([]byte, 5)
n1 := copy(s, a[0:])            // n1 == 6, s == []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:])            // n2 == 4, s == []int{2, 3, 4, 5, 4, 5}
n3 := copy(b, &quot;Hello, World!&quot;)  // n3 == 5, b == []byte(&quot;Hello&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;delete(m, k)  //从字典m中删除元素 m[k] 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内置函数delete从字典m中删除带有键k的元素。&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;complex&lt;/td&gt;
&lt;td&gt;从浮点实部和虚部构造复数值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;real&lt;/td&gt;
&lt;td&gt;提取复数值的实部&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;imag&lt;/td&gt;
&lt;td&gt;提取复数值的虚部&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;complex(realPart, imaginaryPart floatT) complexT
real(complexT) floatT
imag(complexT) floatT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内置函数complex用浮点实部和虚部构造复数值，而real和imag则提取复数值的实部和虚部。&lt;/p&gt;
&lt;p&gt;对于complex，两个参数必须是相同的浮点类型，返回类型是具有相应浮点组成的复数类型。float32用于complex64参数，float64用于complex128参数。如果其中一个参数求值为无类型常量，则首先将其转换为另一个参数的类型。如果两个参数都计算为无类型常量，则它们必须是非复数或其虚部必须为零，并且函数的返回值是无类型复数常量。&lt;/p&gt;
&lt;p&gt;对于real和imag，参数必须是复数类型，返回类型是相应的浮点类型：float32一般为complex64返回类型，float64一般为complex128返回类型。如果参数求值为无类型常量，则它必须是数字，并且函数的返回值是无类型浮点常量。&lt;/p&gt;
&lt;p&gt;real和imag函数一起形成复数的逆，因此对于复数类型Z的值z，z == Z(complex(real(z)，imag(z)))。&lt;/p&gt;
&lt;p&gt;如果这些函数的操作数都是常量，则返回值是常量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = complex(2, -2)             // complex128
const b = complex(1.0, -1.4)        // 无类型complex 常量 1 - 1.4i
x := float32(math.Cos(math.Pi/2))   // float32
var c64 = complex(5, -x)          // complex64
var s uint = complex(1, 0)         // 无类型 complex 常量 1 + 0i 可以转为uint
var rl = real(c64)                // float32
var im = imag(a)                // float64
const c = imag(b)               // 无类型常量 -1.4
&lt;/code&gt;&lt;/pre&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;panic&lt;/td&gt;
&lt;td&gt;用来表示非常严重的不可恢复的异常错误&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recover&lt;/td&gt;
&lt;td&gt;用于从 panic 或 错误场景中恢复&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;func panic(interface{})
func recover() interface{}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;panic和recover两个内置函数，协助报告和处理运行时异常和程序定义的错误。&lt;/p&gt;
&lt;p&gt;在执行函数F时，显式调用panic或者运行时发生panic都会终止F的执行。然后，由F延迟（defer）的任何函数都照常执行。 依此类推，直到执行goroutine中的顶级函数延迟。 此时，程序终止并报告错误条件，包括panic参数的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;panic(42)
panic(&quot;unreachable&quot;)
panic(Error(&quot;cannot parse&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;recover函数允许程序管理发生panic的goroutine的行为。&lt;/p&gt;
&lt;p&gt;另外，Go语言中提供了几个在引导期间有用的内置函数。 这些函数不保证会保留在Go语言中，一般不建议使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print      打印所有参数
println    打印所有参数并换行
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;16.4 递归与回调&lt;/h2&gt;
&lt;p&gt;函数直接或间接调用函数本身，则该函数称为递归函数。使用递归函数时经常会遇到的一个重要问题就是栈溢出：一般出现在大量的递归调用导致的内存分配耗尽。有时我们可以通过循环来解决：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

// Factorial函数递归调用
func Factorial(n uint64)(result uint64) {
    if (n &amp;gt; 0) {
        result = n * Factorial(n-1)
        return result
    }
    return 1
}

// Fac2函数循环计算
func Fac2(n uint64) (result uint64) {
	result = 1
	var un uint64 = 1
	for i := un; i &amp;lt;= n; i++ {
		result *= i
	}
	return
}

func main() {  
    var i uint64= 7
    fmt.Printf(&quot;%d 的阶乘是 %d\n&quot;, i, Factorial(i)) 
    fmt.Printf(&quot;%d 的阶乘是 %d\n&quot;, i, Fac2(i))
}

程序输出：
7 的阶乘是 5040
7 的阶乘是 5040
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go 语言中也可以使用相互调用的递归函数：多个函数之间相互调用形成闭环。因为 Go 语言编译器的特殊性，这些函数的声明顺序可以是任意的。&lt;/p&gt;
&lt;p&gt;Go语言中函数可以作为其它函数的参数进行传递，然后在其它函数内调用执行，一般称之为回调。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

func main() {
	callback(1, Add)
}

func Add(a, b int) {
	fmt.Printf(&quot;%d 与 %d 相加的和是: %d\n&quot;, a, b, a+b)
}

func callback(y int, f func(int, int)) {
	f(y, 2) // 回调函数f
}

程序输出：
1 与 2 相加的和是: 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;16.5 匿名函数&lt;/h2&gt;
&lt;p&gt;函数值字面量是一种表达式，它的值被称为匿名函数。从形式上看当我们不给函数起名字的时候，可以使用匿名函数，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func(x, y int) int { return x + y }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的函数不能够独立存在，但可以被赋值于某个变量，即保存函数的地址到变量中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fplus := func(x, y int) int { return x + y }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后通过变量名对函数进行调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fplus(3, 4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，也可以直接对匿名函数进行调用，注意匿名函数的最后面加上了括号并填入了参数值，如果没有参数，也需要加上空括号，代表直接调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func(x, y int) int { return x + y } (3, 4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是一个计算从 1 到 1 百万整数的总和的匿名函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func() {
    sum := 0
    for i := 1; i &amp;lt;= 1e6; i++ {
        sum += i
    }
}()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数列表的第一对括号必须紧挨着关键字 func，因为匿名函数没有名称。花括号 {} 涵盖着函数体，最后的一对括号表示对该匿名函数的调用。&lt;/p&gt;
&lt;p&gt;下面代码演示了上面的几种情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
package main

import (
	&quot;fmt&quot;
)

func main() {
	fn := func() {
		fmt.Println(&quot;hello&quot;)
	}
	fn()

	fmt.Println(&quot;匿名函数加法求和：&quot;, func(x, y int) int { return x + y }(3, 4))

	func() {
		sum := 0
		for i := 1; i &amp;lt;= 1e6; i++ {
			sum += i
		}
		fmt.Println(&quot;匿名函数加法循环求和：&quot;, sum)
	}()
}

程序输出：
hello
匿名函数加法求和： 7
匿名函数加法循环求和： 500000500000
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;16.6 闭包函数&lt;/h1&gt;
&lt;p&gt;匿名函数同样也被称之为闭包。&lt;/p&gt;
&lt;p&gt;闭包可被允许调用定义在其环境下的变量，可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。闭包继承了函数所声明时的作用域，作用域内的变量都被共享到闭包的环境中，因此这些变量可以在闭包中被操作，直到被销毁。也可以理解为内层函数引用了外层函数中的变量或称为引用了自由变量。&lt;/p&gt;
&lt;p&gt;实质上看，闭包是由函数及其相关引用环境组合而成的实体(即：闭包=函数+引用环境)。闭包在运行时可以有多个实例，不同的引用环境和相同的函数组合可以产生不同的实例。由闭包的实质含义，我们可以推论：闭包获取捕获变量相当于引用传递，而非值传递；对于闭包函数捕获的常量和变量，无论闭包何时何处被调用，闭包都可以使用这些常量和变量，而不用关心它们表面上的作用域。&lt;/p&gt;
&lt;p&gt;换句话说闭包函数可以访问不是它自己内部的变量（这个变量在其它作用域内声明），且这个变量是未赋值的，它在闭包里面赋值。&lt;/p&gt;
&lt;p&gt;我们通过下面代码来看看闭包的使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
package main

import &quot;fmt&quot;

var G int = 7

func main() {
	// 影响全局变量G，代码块状态持续
	y := func() int {
		fmt.Printf(&quot;G: %d, G的地址:%p\n&quot;, G, &amp;amp;G)
		G += 1
		return G
	}
	fmt.Println(y(), y)
	fmt.Println(y(), y)
	fmt.Println(y(), y) //y的地址

	// 影响全局变量G，注意z的匿名函数是直接执行，所以结果不变
	z := func() int {
		G += 1
		return G
	}()
	fmt.Println(z, &amp;amp;z)
	fmt.Println(z, &amp;amp;z)
	fmt.Println(z, &amp;amp;z)

	// 影响外层（自由）变量i，代码块状态持续
	var f = N()
	fmt.Println(f(1), &amp;amp;f)
	fmt.Println(f(1), &amp;amp;f)
	fmt.Println(f(1), &amp;amp;f)

	var f1 = N()
	fmt.Println(f1(1), &amp;amp;f1)

}

func N() func(int) int {
	var i int
	return func(d int) int {
		fmt.Printf(&quot;i: %d, i的地址:%p\n&quot;, i, &amp;amp;i)
		i += d
		return i
	}
}


程序输出：
G: 7, G的地址:0x54b1e8
8 0x490340
G: 8, G的地址:0x54b1e8
9 0x490340
G: 9, G的地址:0x54b1e8
10 0x490340
11 0xc0000500c8
11 0xc0000500c8
11 0xc0000500c8
i: 0, i的地址:0xc0000500e8
1 0xc000078020
i: 1, i的地址:0xc0000500e8
2 0xc000078020
i: 2, i的地址:0xc0000500e8
3 0xc000078020
i: 0, i的地址:0xc000050118
1 0xc000078028
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先强调一点，G是闭包中被捕获的全局变量，因此，对于每一次引用，G的地址都是固定的，i是函数内部局部变量，地址也是固定的，他们都可以被闭包保持状态并修改。还要注意，f和f1是不同的实例，它们的地址是不一样的。&lt;/p&gt;
&lt;h2&gt;16.7 变参函数&lt;/h2&gt;
&lt;p&gt;可变参数也就是不定长参数，支持可变参数列表的函数可以支持任意个传入参数，比如fmt.Println函数就是一个支持可变长参数列表的函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
package main

import &quot;fmt&quot;

func Greeting(who ...string) {
	for k, v := range who {

		fmt.Println(k, v)
	}
}

func main() {
	s := []string{&quot;James&quot;, &quot;Jasmine&quot;}
	Greeting(s...)  // 注意这里切片s... ，把切片打散传入，与s具有相同底层数组的值。
}

程序输出：
0 James
1 Jasmine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_15_errors.md&quot;&gt;第十五章 错误处理&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_17_type.md&quot;&gt;第十七章 Type关键字&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第十五章 错误处理</title><link>https://blog.wemang.com/posts/go/study/42_15_errors/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_15_errors/</guid><pubDate>Sun, 28 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;15.1 错误类型&lt;/h2&gt;
&lt;p&gt;任何时候当你需要一个新的错误类型，都可以用 errors（必须先 import）包的 errors.New 函数接收合适的错误信息来创建，像下面这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;err := errors.New(&quot;math - square root of negative number&quot;)
func Sqrt(f float64) (float64, error) {
if f &amp;lt; 0 {
        return 0, errors.New (&quot;math - square root of negative number&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 fmt 创建错误对象：&lt;/p&gt;
&lt;p&gt;通常你想要返回包含错误参数的更有信息量的字符串，例如：可以用 fmt.Errorf() 来实现：它和 fmt.Printf() 完全一样，接收有一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象。
比如在前面的平方根例子中使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if f &amp;lt; 0 {
    return 0, fmt.Errorf(&quot;square root of negative number %g&quot;, f)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;15.2 Panic&lt;/h2&gt;
&lt;p&gt;在Go语言中 panic() 是一个内置函数，用来表示非常严重的不可恢复的错误。必须要先声明defer，否则不能捕获到异常。普通函数在执行的时候发生了异常，则开始运行defer（如有），defer处理完再返回。&lt;/p&gt;
&lt;p&gt;在多层嵌套的函数调用中调用 panic()，可以马上中止当前函数的执行，所有的 defer 语句都会保证执行并把控制权交还给接收到异常的函数调用者。这样向上冒泡直到最顶层，并执行（每层的） defer，在栈顶处程序崩溃，并在命令行中用传给异常的值报告错误情况：这个终止过程就是 panicking。&lt;/p&gt;
&lt;p&gt;一般不要随意用 panic() 中止程序，必须尽力补救错误让程序能继续执行。&lt;/p&gt;
&lt;p&gt;自定义包中的错误处理和 panicking，这是所有自定义包实现者应该遵守的最佳实践：&lt;/p&gt;
&lt;p&gt;1）在包内部，总是应该从异常中 recover：不允许显式的超出包范围的 panic()&lt;/p&gt;
&lt;p&gt;2）向包的调用者返回错误值。&lt;/p&gt;
&lt;p&gt;recover() 函数的调用仅当它在 defer 函数中被直接调用时才有效。&lt;/p&gt;
&lt;p&gt;下面主函数捕获了异常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

func div(a, b int) {

	defer func() {

		if r := recover(); r != nil {
			fmt.Printf(&quot;捕获到异常：%s\n&quot;, r)
		}
	}()

	if b &amp;lt; 0 {

		panic(&quot;除数需要大于0&quot;)
	}

	fmt.Println(&quot;余数为：&quot;, a/b)

}

func main() {
	// 捕捉内部的异常
	div(10, 0)

	// 捕捉主动的异常
	div(10, -1)
}

程序输出：

捕获到异常：runtime error: integer divide by zero
捕获到异常：除数需要大于0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;15.3 Recover：从异常中恢复&lt;/h2&gt;
&lt;p&gt;recover() 这个内建函数被用于从异常或错误场景中恢复：让程序可以从 panicking 重新获得控制权，停止终止过程进而恢复正常执行。
recover() 只能在 defer 修饰的函数中使用：用于取得异常调用中传递过来的错误值，如果是正常执行，调用 recover() 会返回 nil，且没有其它效果。
总结：异常会导致栈被展开直到 defer 修饰的 recover() 被调用或者程序中止。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func protect(g func()) {
    defer func() {
        log.Println(&quot;done&quot;)
        // 即使有panic，Println也正常执行。
        if err := recover(); err != nil {
        	log.Printf(&quot;run time panic: %v&quot;, err)
        }
    }()
    log.Println(&quot;start&quot;)
    g() //   可能发生运行时错误的地方
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;15.4 有关于defer&lt;/h2&gt;
&lt;p&gt;说到错误处理，就不得不提defer。先说说它的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;规则一 当defer被声明时，其参数就会被实时解析&lt;/li&gt;
&lt;li&gt;规则二 defer执行顺序为先进后出&lt;/li&gt;
&lt;li&gt;规则三 defer可以读取有名返回值，也就是可以改变有名返回参数的值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这三个规则用起来需要注意下，避免出现代码陷阱，下面是具体代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 规则一，当defer被声明时，其参数就会被实时解析
package main

import &quot;fmt&quot;

func main() {
	var i int = 1

	defer fmt.Println(&quot;result =&amp;gt;&quot;, func() int { return i * 2 }())
	i++
	// 输出: result =&amp;gt; 2 (而不是 4)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 规则二 defer执行顺序为先进后出

package main

import &quot;fmt&quot;

func main() {

	defer fmt.Print(&quot; !!! &quot;)
	defer fmt.Print(&quot; world &quot;)
	fmt.Print(&quot; hello &quot;)

}
//输出:  hello  world  !!!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面讲了两条规则，第三条规则其实也不难理解，只要记住是可以改变有名返回值：&lt;/p&gt;
&lt;p&gt;这是由于在Go语言中，return 语句不是原子操作，最先是所有结果值在进入函数时都会初始化为其类型的零值（姑且称为ret赋值），然后执行defer命令,最后才是return操作。如果是有名返回值，返回值变量其实可视为是引用赋值，可以能被defer修改。而在匿名返回值时，给ret的值相当于拷贝赋值，defer命令时不能直接修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func fun1() (i int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面函数签名中的 i 就是有名返回值，如果fun1()中定义了 defer 代码块，是可以改变返回值 i 的，函数返回语句return i 可以简写为 return 。&lt;/p&gt;
&lt;p&gt;这里综合了一下，在下面这个例子里列举了几种情况，可以好好琢磨下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

func main() {
	fmt.Println(&quot;=========================&quot;)
	fmt.Println(&quot;return:&quot;, fun1())

	fmt.Println(&quot;=========================&quot;)
	fmt.Println(&quot;return:&quot;, fun2())
	fmt.Println(&quot;=========================&quot;)

	fmt.Println(&quot;return:&quot;, fun3())
	fmt.Println(&quot;=========================&quot;)

	fmt.Println(&quot;return:&quot;, fun4())
}

func fun1() (i int) {
	defer func() {
		i++
		fmt.Println(&quot;defer2:&quot;, i) // 打印结果为 defer2: 2
	}()

	// 规则二 defer执行顺序为先进后出

	defer func() {
		i++
		fmt.Println(&quot;defer1:&quot;, i) // 打印结果为 defer1: 1
	}()

	// 规则三 defer可以读取有名返回值（函数指定了返回参数名）

	return 0 //这里实际结果为2。如果是return 100呢
}

func fun2() int {
	var i int
	defer func() {
		i++
		fmt.Println(&quot;defer2:&quot;, i) // 打印结果为 defer2: 2
	}()

	defer func() {
		i++
		fmt.Println(&quot;defer1:&quot;, i) // 打印结果为 defer1: 1
	}()
	return i
}

func fun3() (r int) {
	t := 5
	defer func() {
		t = t + 5
		fmt.Println(t)
	}()
	return t
}

func fun4() int {
	i := 8
	// 规则一 当defer被声明时，其参数就会被实时解析
	defer func(i int) {
		i = 99
		fmt.Println(i)
	}(i)
	i = 19
	return i
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面fun1() (i int)有名返回值情况下，return最终返回的实际值和期望的return 0有较大出入。因为在上面fun1() (i int) 中，如果return 100或return 0 ，这样的区别在于i的值实际上分别是100或0。而在上面中，如果return 100，则因为改变了有名返回值i，而defer可以读取有名返回值，所以返回值最终为102，而defer1打印101，defer打印102。因此我们一般直接写为return。&lt;/p&gt;
&lt;p&gt;这点要注意，有时函数可能返回非我们希望的值，所以改为匿名返回也是一种办法。具体请看下面输出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;程序输出：
=========================
defer1: 1
defer2: 2
return: 2
=========================
defer1: 1
defer2: 2
return: 0
=========================
10
return: 5
=========================
99
return: 19

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用defer计算函数执行时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main
import(
        &quot;fmt&quot;
        &quot;time&quot;
)

func main(){
        defer timeCost(time.Now())
        fmt.Println(&quot;start program&quot;)
        time.Sleep(5*time.Second)
        fmt.Println(&quot;finish program&quot;)
}

func timeCost(start time.Time){
        terminal:=time.Since(start)
        fmt.Println(terminal)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外一种计算函数执行时间方法：&lt;/p&gt;
&lt;p&gt;在对比和基准测试中，我们需要知道一个计算执行消耗的时间。最简单的一个办法就是在计算开始之前设置一个起始时候，再由计算结束时的结束时间，最后取出它们的差值，就是这个计算所消耗的时间。想要实现这样的做法，可以使用 time 包中的 Now() 和 Sub 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;start := time.Now()
longCalculation()
end := time.Now()
delta := end.Sub(start)
fmt.Printf(&quot;longCalculation took this amount of time: %s\n&quot;, delta)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_14_flow.md&quot;&gt;第十四章 流程控制&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_16_function.md&quot;&gt;第十六章 函数&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第十四章 流程控制</title><link>https://blog.wemang.com/posts/go/study/42_14_flow/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_14_flow/</guid><pubDate>Sat, 27 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;14.1 switch 语句&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;switch {
    case condition1:
        ...
    case condition2:
        ...
    default:
        ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;switch 语句的第二种形式是不提供任何被判断的值（实际上默认为判断是否为 true），然后在每个 case 分支中进行测试不同的条件。当任一分支的测试结果为 true 时，该分支的代码会被执行。&lt;/p&gt;
&lt;p&gt;switch 语句的第三种形式是包含一个初始化语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;switch initialization {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;switch result := calculate(); {
    case result &amp;lt; 0:
        ...
    case result &amp;gt; 0:
        ...
    default:
        // 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;变量 var1 可以是任何类型，而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数，但必须是相同的类型；或者最终结果为相同类型的表达式。前花括号 &lt;code&gt;{&lt;/code&gt; 必须和 &lt;code&gt;switch&lt;/code&gt; 关键字在同一行。&lt;/p&gt;
&lt;p&gt;您可以同时测试多个可能符合条件的值，使用逗号分割它们，例如：case val1，val2，val3。
一旦成功地匹配到某个分支，在执行完相应代码后就会退出整个 switch 代码块，也就是说您不需要特别使用 break 语句来表示结束。&lt;/p&gt;
&lt;p&gt;如果在执行完每个分支的代码后，还希望继续执行后续分支的代码，可以使用 fallthrough 关键字来达到目的。&lt;/p&gt;
&lt;p&gt;fallthrough强制执行后面的case代码，fallthrough不会判断下一条case的expr结果是否为true。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {

	switch a := 1; {
	case a == 1:
		fmt.Println(&quot;The integer was == 1&quot;)
		fallthrough
	case a == 2:
		fmt.Println(&quot;The integer was == 2&quot;)
	case a == 3:
		fmt.Println(&quot;The integer was == 3&quot;)
		fallthrough
	case a == 4:
		fmt.Println(&quot;The integer was == 4&quot;)
	case a == 5:
		fmt.Println(&quot;The integer was == 5&quot;)
		fallthrough
	default:
		fmt.Println(&quot;default case&quot;)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：
The integer was == 1
The integer was == 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;14.2 select控制&lt;/h2&gt;
&lt;p&gt;select是Go语言中的一个控制结构，类似于switch语句，主要用于处理异步通道操作，所有情况都会涉及通信操作。因此select会监听分支语句中通道的读写操作，当分支中的通道读写操作为非阻塞状态（即能读写）时，将会触发相应的动作。select语句会选择一组可以发送或接收操作中的一个分支继续执行。select没有条件表达式，一直在等待分支进入可运行状态。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;select中的case语句必须是一个channel操作&lt;/p&gt;
&lt;p&gt;select中的default子句总是可运行的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;如果有多个分支都可以运行，select会伪随机公平地选出一个执行，其他分支不会执行。&lt;/li&gt;
&lt;li&gt;如果没有可运行的分支，且有default语句，那么就会执行default的动作。&lt;/li&gt;
&lt;li&gt;如果没有可运行的分支，且没有default语句，select将阻塞，直到某个分支可以运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func main() {
	var c1, c2, c3 chan int
	var i1, i2 int
	select {
	case i1 = &amp;lt;-c1:
		fmt.Printf(&quot;received &quot;, i1, &quot; from c1\n&quot;)
	case c2 &amp;lt;- i2:
		fmt.Printf(&quot;sent &quot;, i2, &quot; to c2\n&quot;)
	case i3, ok := (&amp;lt;-c3): 
		if ok {
			fmt.Printf(&quot;received &quot;, i3, &quot; from c3\n&quot;)
		} else {
			fmt.Printf(&quot;c3 is closed\n&quot;)
		}
	case &amp;lt;-time.After(time.Second * 3): //超时退出
		fmt.Println(&quot;request time out&quot;)
	}
}

// 输出：request time out
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;14.3 for循环&lt;/h2&gt;
&lt;p&gt;最简单的基于计数器的迭代，基本形式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for  初始化语句; 条件语句; 修饰语句 {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这三部分组成的循环的头部，它们之间使用分号 ; 相隔，但并不需要括号 () 将它们括起来。&lt;/p&gt;
&lt;p&gt;您还可以在循环中同时使用多个计数器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for i, j := 0, N; i &amp;lt; j; i, j = i+1, j-1 {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这得益于 Go 语言具有的平行赋值的特性，for 结构的第二种形式是没有头部的条件判断迭代（类似其它语言中的 while 循环），基本形式为：for 条件语句 {}。&lt;/p&gt;
&lt;p&gt;您也可以认为这是没有初始化语句和修饰语句的 for 结构，因此 ;; 便是多余的了&lt;/p&gt;
&lt;p&gt;条件语句是可以被省略的，如 i:=0; ; i++ 或 for { } 或 for ;; { }（;; 会在使用 Gofmt 时被移除）：这些循环的本质就是无限循环。
最后一个形式也可以被改写为 for true { }，但一般情况下都会直接写 for { }。&lt;/p&gt;
&lt;p&gt;如果 for 循环的头部没有条件语句，那么就会认为条件永远为 true，因此循环体内必须有相关的条件判断以确保会在某个时刻退出循环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

func main() {
	a := []int{1, 2, 3, 4, 5, 6}
	for i, j := 0, len(a)-1; i &amp;lt; j; i, j = i+1, j-1 {
		a[i], a[j] = a[j], a[i]
	}

	for j := 0; j &amp;lt; 5; j++ {
		for i := 0; i &amp;lt; 10; i++ {
			if i &amp;gt; 5 {
				break
			}
			fmt.Println(i)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;14.4 for-range 结构&lt;/h2&gt;
&lt;p&gt;for-range 结构是 Go 语言特有的一种迭代结构，它在许多情况下都非常有用。它可以迭代任何一个集合，包括数组（array）和字典（map），同时可以获得每次迭代所对应的索引和值。一般形式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for ix, val := range coll { }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要注意的是，val 始终为集合中对应索引的值的副本，因此它一般只具有只读性质，对它所做的任何修改都不会影响到集合中原有的值（注：如果 val 为指针，则会产生指针的副本，依旧可以修改集合中的原值）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

type field struct {
	name string
}

func (p *field) print() {
	fmt.Println(p.name)
}

func main() {
	data := []field{ {&quot;one&quot;}, {&quot;two&quot;}, {&quot;three&quot;} }

	for _, v := range data {
		go v.print()
	}
	time.Sleep(3 * time.Second)
	// goroutines （可能）显示: three, three, three
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当前的迭代变量作为匿名goroutine的参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

func main() {  
    data := []string{&quot;one&quot;, &quot;two&quot;, &quot;three&quot;}

    for _, v := range data {
        go func(in string) {
            fmt.Println(in)
        }(v)
    }

    time.Sleep(3 * time.Second)
    // goroutines输出: one, two, three
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个字符串是 Unicode 编码的字符（或称之为 rune）集合，因此您也可以用它迭代字符串：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for pos, char := range str {
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;if&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If语句由布尔表达式后紧跟一个或多个语句组成，注意布尔表达式不用()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if 布尔表达式 {
   /* 在布尔表达式为 true 时执行 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;break&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一个 break 的作用范围为该语句出现后的最内部的结构，它可以被用于任何形式的 for 循环（计数器、条件判断等）。
但在 switch 或 select 语句中，break 语句的作用结果是跳过整个代码块，执行后续的代码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;continue&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;关键字 continue 忽略剩余的循环体而直接进入下一次循环的过程，但不是无条件执行下一次循环，执行之前依旧需要满足循环的判断条件。
关键字 continue 只能被用于 for 循环中。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;label&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;for、switch 或 select 语句都可以配合标签（label）形式的标识符使用，即某一行第一个以冒号（:）结尾的单词（Gofmt 会将后续代码自动移至下一行）
（标签的名称是大小写敏感的，为了提升可读性，一般建议使用全部大写字母）
continue 语句指向 LABEL1，当执行到该语句的时候，就会跳转到 LABEL1 标签的位置。&lt;/p&gt;
&lt;p&gt;使用标签和 Goto 语句是不被鼓励的：它们会很快导致非常糟糕的程序设计，而且总有更加可读的替代方案来实现相同的需求。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_13_map.md&quot;&gt;第十三章 字典(Map)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_15_errors.md&quot;&gt;第十五章 错误处理&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第十三章 字典(Map)</title><link>https://blog.wemang.com/posts/go/study/42_13_map/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_13_map/</guid><pubDate>Fri, 26 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;13.1 字典(Map)&lt;/h2&gt;
&lt;p&gt;map是一种元素对的无序集合，一组称为元素value，另一组为唯一键索引key。 未初始化map的值为nil。map 是引用类型，可以使用如下声明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var map1 map[keytype]valuetype
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（[keytype] 和 valuetype 之间允许有空格，但是 Gofmt 移除了空格）&lt;/p&gt;
&lt;p&gt;在声明的时候不需要知道 map 的长度，map 是可以动态增长的。&lt;/p&gt;
&lt;p&gt;key 可以是任意可以用 == 或者 != 操作符比较的类型，比如 string、int、float。所以数组、函数、字典、切片和结构体不能作为 key (含有数组切片的结构体不能作为 key，只包含内建类型的 struct 是可以作为 key 的），但是指针和接口类型可以。&lt;/p&gt;
&lt;p&gt;value 可以是任意类型的；通过使用空接口类型，我们可以存储任意值，但是使用这种类型作为值时需要先做一次类型断言。map 也可以用函数作为自己的值，这样就可以用来做分支结构：key 用来选择要执行的函数。&lt;/p&gt;
&lt;p&gt;map 传递给函数的代价很小：在 32 位机器上占 4 个字节，64 位机器上占 8 个字节，无论实际上存储了多少数据。通过 key 在 map 中寻找值是很快的，比线性查找快得多，但是仍然比从数组和切片的索引中直接读取要慢 100 倍；所以如果你很在乎性能的话还是建议用切片来解决问题。&lt;/p&gt;
&lt;p&gt;map 可以用 &lt;code&gt;{key1: val1, key2: val2}&lt;/code&gt; 的描述方法来初始化，就像数组和结构体一样。&lt;/p&gt;
&lt;p&gt;map 是引用类型的，内存用 make 方法来分配。map 的初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var map1 = make(map[keytype]valuetype)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;map 容量：
和数组不同，map 可以根据新增的 key-value 对动态的伸缩，因此它不存在固定长度或者最大限制。但是你也可以选择标明 map 的初始容量 capacity，就像这样：make(map[keytype]valuetype，cap)。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;map2 := make(map[string]float32, 100)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 map 增长到容量上限的时候，如果再增加新的 key-value 对，map 的大小会自动加 1。所以出于性能的考虑，对于大的 map 或者会快速扩张的 map，即使只是大概知道容量，也最好先标明。&lt;/p&gt;
&lt;p&gt;在一个 nil 的slice中添加元素是没问题的，但对一个map做同样的事将会生成一个运行时的panic。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;可正常运行：

package main
func main() {  
    var s []int
    s = append(s, 1)
}

会发生错误：

package main
func main() {  
    var m map[string]int
    m[&quot;one&quot;] = 1 // 错误

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;map的key访问，val1, isPresent := map1[key1]  或者 val1 = map1[key1] 的方法获取 key1 对应的值 val1。&lt;/p&gt;
&lt;p&gt;一般判断是否某个key存在，不使用值判断，而使用下面的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if _, ok := x[&quot;two&quot;]; !ok {
        fmt.Println(&quot;no entry&quot;)
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有一些定义 map 的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 声明但未初始化map，此时是map的零值状态
map1 := make(map[string]string, 5)

map2 := make(map[string]string)

// 创建了初始化了一个空的的map，这个时候没有任何元素
map3 := map[string]string{}

// map中有三个值
map4 := map[string]string{&quot;a&quot;: &quot;1&quot;, &quot;b&quot;: &quot;2&quot;, &quot;c&quot;: &quot;3&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 map1 中删除 key1，直接 delete(map1, key1) 就可以。如果 key1 不存在，该操作不会产生错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;delete(map4, &quot;a&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;map 默认是无序的，不管是按照 key 还是按照 value 默认都不排序。如果你想为 map 排序，需要将 key（或者 value）拷贝到一个切片，再对切片排序（使用 sort 包）。&lt;/p&gt;
&lt;h2&gt;13.2 range语句中的值&lt;/h2&gt;
&lt;p&gt;在&quot;range&quot;语句中生成的数据的值是真实集合元素的拷贝，它们不是原有元素的引用。这意味着更新这些值将不会修改原来的数据。同时也意味着使用这些值的地址将不会得到原有数据的指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main
import &quot;fmt&quot;
func main() {  
    data := []int{1, 2, 3}
    for _, v := range data {
        v *= 10 // 通常数据项不会改变
    }
    fmt.Println(&quot;data:&quot;, data) // 程序输出: [1 2 3]
}

程序输出：
data: [1 2 3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你需要更新原有集合中的数据，使用索引操作符来获得数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main
import &quot;fmt&quot;
func main() {  
    data := []int{1, 2, 3}
    for i, _ := range data {
        data[i] *= 10
    }

    fmt.Println(&quot;data:&quot;, data) // 程序输出 data: [10 20 30]
}

程序输出：
data: [10 20 30]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_12_slice.md&quot;&gt;第十二章 切片(slice)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_14_flow.md&quot;&gt;第十四章 流程控制&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第十二章 切片(slice)</title><link>https://blog.wemang.com/posts/go/study/42_12_slice/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_12_slice/</guid><pubDate>Thu, 25 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;12.1 切片(slice)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;切片（slice）&lt;/strong&gt; 是对底层数组一个连续片段的引用，所以切片是一个引用类型。切片提供对该数组中编号的元素序列的访问。未初始化切片的值为nil。&lt;/p&gt;
&lt;p&gt;与数组一样，切片是可索引的并且具有长度。切片s的长度可以通过内置函数len() 获取;与数组不同，切片的长度可能在执行期间发生变化。元素可以通过整数索引0到len（s）-1来寻址。我们可以把切片看成是一个长度可变的数组。&lt;/p&gt;
&lt;p&gt;切片提供了计算容量的函数 cap() ，可以测量切片最大长度。切片的长度永远不会超过它的容量，所以对于切片 s 来说，这个不等式永远成立：&lt;code&gt;0 &amp;lt;= len(s) &amp;lt;= cap(s)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;一旦初始化，切片始终与保存其元素的基础数组相关联。因此，切片会和与其拥有同一基础数组的其他切片共享存储;相比之下，不同的数组总是代表不同的存储。&lt;/p&gt;
&lt;p&gt;切片下面的数组可以延伸超过切片的末端。容量是切片长度与切片之外的数组长度的总和。&lt;/p&gt;
&lt;p&gt;使用内置函数make()可以给切片初始化，该函数指定切片类型和指定长度和可选容量的参数。&lt;/p&gt;
&lt;p&gt;切片与数组相比较：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为切片是引用，所以它们不需要使用额外的内存并且比使用数组更有效率，所以在 Go 代码中切片比数组更常用。&lt;/p&gt;
&lt;p&gt;声明切片的格式是： var identifier []type（不需要说明长度）。一个切片在未初始化之前默认为 nil，长度为 0。&lt;/p&gt;
&lt;p&gt;切片的初始化格式是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var slice1 []type = arr1[start:end]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这表示 slice1 是由数组 arr1 从 start 索引到 end-1 索引之间的元素构成的子集（切分数组，start:end 被称为切片表达式）。&lt;/p&gt;
&lt;p&gt;切片也可以用类似数组的方式初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var x = []int{2, 3, 5, 7, 11}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就创建了一个长度为 5 的数组并且创建了一个相关切片。&lt;/p&gt;
&lt;p&gt;当相关数组还没有定义时，我们可以使用 make() 函数来创建一个切片，同时创建好相关数组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var slice1 []type = make([]type, len,cap)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以简写为 slice1 := make([]type, len)，这里 len 是数组的长度并且也是切片的初始长度。cap是容量，其中cap是可选参数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;v := make([]int, 10, 50)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样分配一个有 50 个int值的数组，并且创建了一个长度为10，容量为50的切片 v，该切片指向数组的前 10 个元素。&lt;/p&gt;
&lt;p&gt;以上我们列举了三种切片初始化方式，这三种方式都比较常用。&lt;/p&gt;
&lt;p&gt;如果从数组或者切片中生成一个新的切片，我们可以使用下面的表达式：&lt;/p&gt;
&lt;p&gt;a[low : high : max]     max-low的结果表示容量，high-low的结果表示长度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里t的容量（capacity）是5-1=4 ，长度是2。&lt;/p&gt;
&lt;p&gt;如果切片取值时索引值大于长度会导致panic错误发生，即使容量远远大于长度也没有用，如下面代码所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
	sli := make([]int, 5, 10)
	fmt.Printf(&quot;切片sli长度和容量：%d, %d\n&quot;, len(sli), cap(sli))
	fmt.Println(sli)
	newsli := sli[:cap(sli)]
	fmt.Println(newsli)

	var x = []int{2, 3, 5, 7, 11}
	fmt.Printf(&quot;切片x长度和容量：%d, %d\n&quot;, len(x), cap(x))

	a := [5]int{1, 2, 3, 4, 5}
	t := a[1:3:5] // a[low : high : max]  max-low的结果表示容量  high-low为长度
	fmt.Printf(&quot;切片t长度和容量：%d, %d\n&quot;, len(t), cap(t))

	// fmt.Println(t[2]) // panic ，索引不能超过切片的长度
}

程序输出：
切片sli长度和容量：5, 10
[0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0]
切片x长度和容量：5, 5
切片t长度和容量：2, 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;12.2 切片重组(reslice)&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;slice1 := make([]type, start_length, capacity)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过改变切片长度得到新切片的过程称之为切片重组 reslicing，做法如下：slice1 = slice1[0:end]，其中 end 是新的末尾索引（即长度）。&lt;/p&gt;
&lt;p&gt;当我们在一个切片基础上重新划分一个切片时，新的切片会继续引用原有切片的数组。如果你忘了这个行为的话，在你的应用分配大量临时的切片用于创建新的切片来引用原有数据的一小部分时，会导致难以预期的内存使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func get() []byte {  
    raw := make([]byte, 10000)
    fmt.Println(len(raw), cap(raw), &amp;amp;raw[0]) // 显示: 10000 10000 数组首字节地址
    return raw[:3]  // 10000个字节实际只需要引用3个，其他空间浪费
}

func main() {  
    data := get()
    fmt.Println(len(data), cap(data), &amp;amp;data[0]) // 显示: 3 10000 数组首字节地址
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了避免这个陷阱，我们需要从临时的切片中使用内置函数copy()，拷贝数据（而不是重新划分切片）到新切片。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func get() []byte {
	raw := make([]byte, 10000)
	fmt.Println(len(raw), cap(raw), &amp;amp;raw[0]) // 显示: 10000 10000 数组首字节地址
	res := make([]byte, 3)
	copy(res, raw[:3]) // 利用copy 函数复制，raw 可被GC释放
	return res
}

func main() {
	data := get()
	fmt.Println(len(data), cap(data), &amp;amp;data[0]) // 显示: 3 3 数组首字节地址
}

程序输出：
10000 10000 0xc000086000
3 3 0xc000050098
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;append()内置函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func append(s S, x ...T) S  // T是S元素类型
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;append()函数将 0 个或多个具有相同类型S的元素追加到切片s后面并且返回新的切片；追加的元素必须和原切片的元素同类型。如果s的容量不足以存储新增元素，append()会分配新的切片来保证已有切片元素和新增元素的存储。&lt;/p&gt;
&lt;p&gt;因此，append()函数返回的切片可能已经指向一个不同的相关数组了。append()函数总是返回成功，除非系统内存耗尽了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s0 := []int{0, 0}
s1 := append(s0, 2)                // append 单个元素     s1 == []int{0, 0, 2}
s2 := append(s1, 3, 5, 7)          // append 多个元素    s2 == []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...)            // append 一个切片     s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...)   // append 切片片段    s4 == []int{3, 5, 7, 2, 3, 5, 7, 0, 0}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;append()函数操作如果导致分配新的切片来保证已有切片元素和新增元素的存储，也就是返回的切片可能已经指向一个不同的相关数组了，那么新的切片已经和原来切片没有任何关系，即使修改了数据也不会同步。&lt;/p&gt;
&lt;p&gt;append()函数操作后，有没有生成新的切片需要看原有切片的容量是否足够。&lt;/p&gt;
&lt;h2&gt;12.3 陈旧的切片(Stale Slices)&lt;/h2&gt;
&lt;p&gt;多个切片可以引用同一个底层数组。在某些情况下，在一个切片中添加新的数据，在原有数组无法保持更多新的数据时，将导致分配一个新的数组。而现在其他的切片还指向老的数组（和老的数据）。&lt;/p&gt;
&lt;p&gt;上一节我们也说了：append()函数操作后，有没有生成新的切片需要看原有切片的容量是否足够。&lt;/p&gt;
&lt;p&gt;下面，我们看看这个过程是怎么产生的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
	s1 := []int{1, 2, 3}
	fmt.Println(len(s1), cap(s1), s1) // 输出 3 3 [1 2 3]
	s2 := s1[1:]
	fmt.Println(len(s2), cap(s2), s2) // 输出 2 2 [2 3]
	for i := range s2 {
		s2[i] += 20
	}
	// s2的修改会影响到数组数据，s1输出新数据
	fmt.Println(s1) // 输出 [1 22 23]
	fmt.Println(s2) // 输出 [22 23]

	s2 = append(s2, 4) // append  s2容量为2，这个操作导致了切片 s2扩容，会生成新的底层数组。

	for i := range s2 {
		s2[i] += 10
	}
	// s1 的数据现在是老数据，而s2扩容了，复制数据到了新数组，他们的底层数组已经不是同一个了。
	fmt.Println(len(s1), cap(s1), s1) // 输出3 3 [1 22 23]
	fmt.Println(len(s2), cap(s2), s2) // 输出3 4 [32 33 14]
}


程序输出：
3 3 [1 2 3]
2 2 [2 3]
[1 22 23]
[22 23]
3 3 [1 22 23]
3 4 [32 33 14]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_11_array.md&quot;&gt;第十一章 数组(Array)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_13_map.md&quot;&gt;第十三章 字典(Map)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第十一章 数组(Array)</title><link>https://blog.wemang.com/posts/go/study/42_11_array/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_11_array/</guid><pubDate>Wed, 24 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;11.1 数组(Array)&lt;/h2&gt;
&lt;p&gt;数组是具有相同唯一类型的一组已编号且长度固定的数据项序列（这是一种同构的数据结构）；这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式，并且必须是一个非负整数。&lt;/p&gt;
&lt;p&gt;数组长度也是数组类型的一部分，所以[5]int和[10]int是属于不同类型的。&lt;/p&gt;
&lt;p&gt;注意事项：如果我们想让数组元素类型为任意类型的话可以使用空接口interface{}作为类型。当使用值时我们必须先做一个类型判断。&lt;/p&gt;
&lt;p&gt;在Go语言中，可以定义一维数组或者多维数组。&lt;/p&gt;
&lt;p&gt;一维数组声明以及初始化常见方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var arrAge  = [5]int{18, 20, 15, 22, 16}
var arrName = [5]string{3: &quot;Chris&quot;, 4: &quot;Ron&quot;} //指定索引位置初始化 
// {&quot;&quot;,&quot;&quot;,&quot;&quot;,&quot;Chris&quot;,&quot;Ron&quot;}
var arrCount = [4]int{500, 2: 100} //指定索引位置初始化 {500,0,100,0}
var arrLazy = [...]int{5, 6, 7, 8, 22} //数组长度初始化时根据元素多少确定
var arrPack = [...]int{10, 5: 100} //指定索引位置初始化，数组长度与此有关 {10,0,0,0,0,100}
var arrRoom [20]int
var arrBed = new([20]int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数组在声明时需要确定长度，但是也可以采用上面不定长数组的方式声明，在初始化时会自动确定好数组的长度。上面 arrPack 声明中 len(arrPack) 结果为6 ，表明初始化时已经确定了数组长度。而arrRoom和arrBed这两个数组的所有元素这时都为0，这是因为每个元素是一个整型值，当声明数组时所有的元素都会被自动初始化为默认值 0。&lt;/p&gt;
&lt;p&gt;Go 语言中的数组是一种值类型（不像 C/C++ 中是指向首元素的指针），所以可以通过 new() 来创建：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var arr1 = new([5]int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么这种方式和 var arr2 [5]int 的区别是什么呢？arr1 的类型是 *[5]int，而 arr2的类型是 [5]int。在Go语言中，数组的长度都算在类型里。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

func main() {

	var arr1 = new([5]int)
	arr := arr1
	arr1[2] = 100
	fmt.Println(arr1[2], arr[2])

	var arr2 [5]int
	newarr := arr2
	arr2[2] = 100
	fmt.Println(arr2[2], newarr[2])
}

程序输出：
100 100
100 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面代码结果可以看到，new([5]int)创建的是数组指针，arr其实和arr1指向同一地址，故而修改arr1时arr同样也生效。而newarr是由arr2值传递（拷贝），故而修改任何一个都不会改变另一个的值。在写函数或方法时，如果参数是数组，需要注意参数长度不能过大。&lt;/p&gt;
&lt;p&gt;由于把一个大数组传递给函数会消耗很多内存（值传递），在实际中我们通常有两种方法可以避免这种现象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;传递数组的指针
使用切片
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而通常使用切片是第一选择，有关切片的使用，请看后面有关章节。&lt;/p&gt;
&lt;p&gt;多维数组在Go语言中也是支持的，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[...][5]int{ {10, 20}, {30, 40} }	   // len() 长度根据实际初始化时数据的长度来定，这里为2
[3][5]int				   // len() 长度为3
[2][2][2]float64			   // 可以这样理解 [2]([2]([2]float64))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在定义多维数组时，仅第一维允许使用“…”，而内置函数len和cap也都返回第一维度长度。定义数组时使用“…”表示长度，表示初始化时的实际长度来确定数组的长度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;b := [...][5]int{ { 10, 20 }, { 30, 40, 50, 60 } }

fmt.Println(b[1][3], len(b)) //60 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数组元素可以通过索引（下标）来读取（或者修改），索引从 0 开始，第一个元素索引为 0，第二个索引为 1，以此类推。（数组以 0 开始在所有类 C 语言中是相似的）。元素的数目，也称为长度或者数组大小必须是固定的并且在声明该数组时就给出（编译时需要知道数组长度以便分配内存）；数组大小最大为 2Gb。&lt;/p&gt;
&lt;p&gt;遍历数组的方法既可以for 条件循环，也可以使用 for-range。这两种 for 结构对于切片（slices）来说也同样适用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var arrAge = [5]int{18, 20, 15, 22, 16}
	for i, v := range arrAge {
		fmt.Printf(&quot;%d 的年龄： %d\n&quot;, i, v)
}

0 的年龄： 18
1 的年龄： 20
2 的年龄： 15
3 的年龄： 22
4 的年龄： 16

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多维数组的遍历需要使用多层的循环嵌套，这里就不举例了。&lt;/p&gt;
&lt;p&gt;另外，如数组元素类型支持”==，!=”操作符，那么数组也支持此操作，但如果数组类型不一样则不支持（需要长度和数据类型一致，否则编译不通过）。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var arrRoom [20]int
var arrBed [20]int

println(arrRoom == arrBed) //true

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_10_string.md&quot;&gt;第十章 string&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_12_slice.md&quot;&gt;第十二章 切片(slice)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第十章 string</title><link>https://blog.wemang.com/posts/go/study/42_10_string/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_10_string/</guid><pubDate>Tue, 23 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;10.1 字符串介绍&lt;/h2&gt;
&lt;p&gt;Go 语言中可以使用反引号或者双引号来定义字符串。反引号表示原生的字符串，即不进行转义。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;双引号：字符串使用双引号括起来，其中的相关的转义字符将被替换。例如：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;str := &quot;Hello World! \n Hello Gopher! \n&quot;

输出：
Hello World! 
Hello Gopher!
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;反引号：字符串使用反引号括起来，其中的相关的转义字符不会被替换。例如：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;str :=  `Hello World! \n Hello Gopher! \n` 

输出：
Hello World! \nHello Gopher! \n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;双引号中的转义字符被替换，而反引号中原生字符串中的 \n 会被原样输出。&lt;/p&gt;
&lt;p&gt;Go 语言中的string类型是一种值类型，存储的字符串是不可变的，如果要修改string内容需要将string转换为[]byte或[]rune，并且修改后的string内容是重新分配的。&lt;/p&gt;
&lt;p&gt;那么byte和rune的区别是什么(下面写法是type别名):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type byte = uint8
type rune = int32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面的定义中我们可清楚看到两者的区别。&lt;/p&gt;
&lt;p&gt;而string类型的零值是为长度为零的字符串，即空字符串 &quot;&quot;。&lt;/p&gt;
&lt;p&gt;一般的比较运算符&lt;code&gt;（==、!=、&amp;lt;、&amp;lt;=、&amp;gt;=、&amp;gt;）&lt;/code&gt;通过在内存中按字节比较来实现字符串的对比。你可以通过函数 len() 来获取字符串所占的字节长度，例如：len(str)。&lt;/p&gt;
&lt;p&gt;字符串的内容（纯字节）可以通过标准索引法来获取，在中括号 [] 内写入索引，索引从 0 开始计数：&lt;/p&gt;
&lt;p&gt;字符串 str 的第 1 个字节：str[0]
第 i 个字节：str[i - 1]
最后 1 个字节：str[len(str)-1]&lt;/p&gt;
&lt;p&gt;需要注意的是，在Go语言代码使用 UTF-8 编码，同时标识符也支持 Unicode 字符。在标准库 unicode 包中，提供了对 Unicode 相关编码、解码的支持。而UTF8编码由Go语言之父Ken Thompson和Rob Pike共同发明的，现在已经是Unicode的标准。&lt;/p&gt;
&lt;p&gt;Go语言默认使用UTF-8编码，对Unicode的支持非常好。但这也带来一个问题，也就是很多资料中提到的“获取字符串长度”的问题。内置的len()函数获取的是每个字符的UTF-8编码的长度和，而不是直接的字符数量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;unicode/utf8&quot;
)

func main() {

	s := &quot;其实就是rune&quot;
	fmt.Println(len(s))                    // &quot;16&quot;
	fmt.Println(utf8.RuneCountInString(s)) // &quot;8&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如字符串含有中文等字符，我们可以看到每个中文字符的索引值相差3。下面代码同时说明了在for range循环处理字符时，不是按照字节的方式来处理的。v其实际上是一个rune类型值。实际上，Go语言的range循环在处理字符串的时候，会自动隐式解码UTF8字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

func main() {
	s := &quot;Go语言四十二章经&quot;
	for k, v := range s {
		fmt.Printf(&quot;k：%d,v：%c == %d\n&quot;, k, v, v)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：
k：0,v：G == 71
k：1,v：o == 111
k：2,v：语 == 35821
k：5,v：言 == 35328
k：8,v：四 == 22235
k：11,v：十 == 21313
k：14,v：二 == 20108
k：17,v：章 == 31456
k：20,v：经 == 32463
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意事项：&lt;/p&gt;
&lt;p&gt;获取字符串中某个字节的地址的行为是非法的，例如：&amp;amp;str[i]。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;10.2 字符串拼接&lt;/h2&gt;
&lt;p&gt;可以通过以下方式来对代码中多行的字符串进行拼接。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接使用运算符&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;str := &quot;Beginning of the string &quot; +
&quot;second part of the string&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于编译器行尾自动补全分号的缘故，加号 + 必须放在第一行。
拼接的简写形式 += 也可以用于字符串：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s := &quot;hel&quot; + &quot;lo, &quot;
s += &quot;world!&quot;
fmt.Println(s) // 输出 “hello, world!”
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;里面的字符串都是不可变的，每次运算都会产生一个新的字符串，所以会产生很多临时的无用的字符串，不仅没有用，还会给 GC 带来额外的负担，所以性能比较差。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fmt.Sprintf()&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;fmt.Sprintf(&quot;%d:%s&quot;, 2018, &quot;年&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内部使用 []byte 实现，不像直接运算符这种会产生很多临时的字符串，但是内部的逻辑比较复杂，有很多额外的判断，还用到了 interface，所以性能一般。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;strings.Join()&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;strings.Join([]string{&quot;hello&quot;, &quot;world&quot;}, &quot;, &quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Join会先根据字符串数组的内容，计算出一个拼接之后的长度，然后申请对应大小的内存，一个一个字符串填入，在已有一个数组的情况下，这种效率会很高，但是本来没有，去构造这个数据的代价也不小。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bytes.Buffer&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;var buffer bytes.Buffer
buffer.WriteString(&quot;hello&quot;)
buffer.WriteString(&quot;, &quot;)
buffer.WriteString(&quot;world&quot;)

fmt.Print(buffer.String())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个比较理想，可以当成可变字符使用，对内存的增长也有优化，如果能预估字符串的长度，还可以用 buffer.Grow() 接口来设置 capacity。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;strings.Builder&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;var b1 strings.Builder
b1.WriteString(&quot;ABC&quot;)
b1.WriteString(&quot;DEF&quot;)

fmt.Print(b1.String())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;strings.Builder 内部通过 slice 来保存和管理内容。slice 内部则是通过一个指针指向实际保存内容的数组。strings.Builder 同样也提供了 Grow() 来支持预定义容量。当我们可以预定义我们需要使用的容量时，strings.Builder 就能避免扩容而创建新的 slice 了。strings.Builder是非线程安全，性能上和 bytes.Buffer 相差无几。&lt;/p&gt;
&lt;h2&gt;10.3 有关string处理&lt;/h2&gt;
&lt;p&gt;标准库中有四个包对字符串处理尤为重要：bytes、strings、strconv和unicode包。&lt;/p&gt;
&lt;p&gt;strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。&lt;/p&gt;
&lt;p&gt;bytes包也提供了很多类似功能的函数，但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的，因此逐步构建字符串会导致很多分配和复制。在这种情况下，使用bytes.Buffer类型将会更有效，稍后我们将展示。&lt;/p&gt;
&lt;p&gt;strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换，还提供了双引号转义相关的转换。&lt;/p&gt;
&lt;p&gt;unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能，它们用于给字符分类。&lt;/p&gt;
&lt;p&gt;strings 包提供了很多操作字符串的简单函数，通常一般的字符串操作需求都可以在这个包中找到。下面简单举几个例子：&lt;/p&gt;
&lt;p&gt;判断是否以某字符串打头/结尾
strings.HasPrefix(s, prefix string) bool
strings.HasSuffix(s, suffix string) bool&lt;/p&gt;
&lt;p&gt;字符串分割
strings.Split(s, sep string) []string&lt;/p&gt;
&lt;p&gt;返回子串索引
strings.Index(s, substr string) int
strings.LastIndex 最后一个匹配索引&lt;/p&gt;
&lt;p&gt;字符串连接
strings.Join(a []string, sep string) string
另外可以直接使用“+”来连接两个字符串&lt;/p&gt;
&lt;p&gt;字符串替换
strings.Replace(s, old, new string, n int) string&lt;/p&gt;
&lt;p&gt;字符串转化为大小写
strings.ToUpper(s string) string
strings.ToLower(s string) string&lt;/p&gt;
&lt;p&gt;统计某个字符在字符串出现的次数
strings.Count(s, substr string) int&lt;/p&gt;
&lt;p&gt;判断字符串的包含关系
strings.Contains(s, substr string) bool&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_09_operator.md&quot;&gt;第九章 运算符&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_11_array.md&quot;&gt;第十一章 数组(Array)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第九章 运算符</title><link>https://blog.wemang.com/posts/go/study/42_09_operator/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_09_operator/</guid><pubDate>Mon, 22 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;9.1 内置运算符&lt;/h2&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;li&gt;其他运算符&lt;/li&gt;
&lt;/ul&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;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;/td&gt;
&lt;td&gt;A + B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;相减&lt;/td&gt;
&lt;td&gt;A - B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;*&lt;/td&gt;
&lt;td&gt;相乘&lt;/td&gt;
&lt;td&gt;A * B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;相除&lt;/td&gt;
&lt;td&gt;B / A 结果还是整数 8/3=2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;%&lt;/td&gt;
&lt;td&gt;求余&lt;/td&gt;
&lt;td&gt;B % A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;++&lt;/td&gt;
&lt;td&gt;自增&lt;/td&gt;
&lt;td&gt;A++ 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;自减&lt;/td&gt;
&lt;td&gt;A--&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;th&gt;示意&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;==&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;检查两个值是否相等。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(A == B) &lt;/code&gt;为 False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;!=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;检查两个值是否不相等。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(A != B)&lt;/code&gt; 为 True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt;&lt;/td&gt;
&lt;td&gt;检查左边值是否大于右边值。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(A &amp;gt; B)&lt;/code&gt; 为 False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;检查左边值是否小于右边值。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(A &amp;lt; B)&lt;/code&gt; 为 True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;检查左边值是否大于等于右边值。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(A &amp;gt;= B)&lt;/code&gt; 为 False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;检查左边值是否小于等于右边值。&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(A &amp;lt;= B)&lt;/code&gt; 为 True&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;逻辑与&lt;/td&gt;
&lt;td&gt;如果两边的操作数都是 True，则条件 True，否则为 False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;||&lt;/td&gt;
&lt;td&gt;逻辑或&lt;/td&gt;
&lt;td&gt;如果两边的操作数有一个 True，则条件 True，否则为 False&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;!&lt;/td&gt;
&lt;td&gt;逻辑非&lt;/td&gt;
&lt;td&gt;如果条件为 True，则逻辑 NOT 条件 False，否则为 True&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Go语言的位运算符：&lt;/p&gt;
&lt;p&gt;位运算符对整数在内存中的二进制位进行操作。
下表列出了位运算符 &amp;amp;，|，和 ^ 的计算：&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;&amp;amp; 与&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;p&lt;/td&gt;
&lt;td&gt;q&lt;/td&gt;
&lt;td&gt;p  &amp;amp; q&lt;/td&gt;
&lt;td&gt;p | q&lt;/td&gt;
&lt;td&gt;p ^ q&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Go 语言支持的位运算符含义。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&amp;amp;	按位与运算符&quot;&amp;amp;&quot;是双目运算符。 其功能是参与运算的两数各对应的二进位相与。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;|	按位或运算符&quot;|&quot;是双目运算符。 其功能是参与运算的两数各对应的二进位相或。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;^	按位异或运算符&quot;^&quot;是双目运算符。 其功能是参与运算的两数各对应的二进位相异或，当两对应的二进位相异时，结果为1。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;&amp;lt;&lt;/code&gt;	左移运算符&lt;code&gt;&quot;&amp;lt;&amp;lt;&quot;&lt;/code&gt;是双目运算符。左移n位就是乘以2的n次方。 其功能把&lt;code&gt;&quot;&amp;lt;&amp;lt;&quot;&lt;/code&gt;左边的运算数的各二进位全部左移若干位，由&lt;code&gt;&quot;&amp;lt;&amp;lt;&quot;&lt;/code&gt;右边的数指定移动的位数，高位丢弃，低位补0。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;gt;&amp;gt;	右移运算符&quot;&amp;gt;&amp;gt;&quot;是双目运算符。右移n位就是除以2的n次方。 其功能是把&quot;&amp;gt;&amp;gt;&quot;左边的运算数的各二进位全部右移若干位，&quot;&amp;gt;&amp;gt;&quot;右边的数指定移动的位数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&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;th&gt;示意&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;=&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;code&gt;+=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相加后再赋值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;C += A 等于 C = C + A&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相减后再赋值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;C -= A 等于 C = C - A&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相乘后再赋值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;C *= A 等于 C = C * A&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;相除后再赋值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;C /= A 等于 C = C / A&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;求余后再赋值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;C %= A 等于 C = C % A&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;&amp;lt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;左移后赋值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;C &amp;lt;&amp;lt;= 2 等于 C = C &amp;lt;&amp;lt; 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;&amp;gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;右移后赋值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;C &amp;gt;&amp;gt;= 2 等于 C = C &amp;gt;&amp;gt; 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按位与后赋值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;C &amp;amp;= 2 等于 C = C &amp;amp; 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;^=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按位异或后赋值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;C ^= 2 等于 C = C ^ 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;#124;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;按位或后赋值&lt;/td&gt;
&lt;td&gt;&lt;code&gt;C &amp;amp;#124;= 2 等于 C = C &amp;amp;#124; 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;&amp;amp;&lt;/td&gt;
&lt;td&gt;返回变量存储地址	&amp;amp;a; 将给出变量的实际地址。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;*&lt;/td&gt;
&lt;td&gt;指针变量。	        *a; 是一个指针变量&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;9.2 运算符优先级&lt;/h2&gt;
&lt;p&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;7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;^ !&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;* / % &amp;lt;&amp;lt; &amp;gt;&amp;gt; &amp;amp; &amp;amp;^&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+ - &amp;amp;#124; ^&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;== != &amp;lt; &amp;lt;= &amp;gt;= &amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;-&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;#124;&amp;amp;#124;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;当然，你可以通过使用括号来临时提升某个表达式的整体运算优先级。&lt;/p&gt;
&lt;h2&gt;9.3 几个特殊运算符&lt;/h2&gt;
&lt;p&gt;位清除 &amp;amp;^：&lt;/p&gt;
&lt;p&gt;将指定位置上的值设置为 0。将运算符左边数据相异的位保留，相同位清零 ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;X=2
Y=4
x&amp;amp;^y==x&amp;amp;(^y)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先我们先换算成2进制  0000 0010 &amp;amp;^ 0000 0100 = 0000 0010 如果y bit位上的数是0则取x上对应位置的值， 如果y bit位上为1则结果位上取0&lt;/p&gt;
&lt;p&gt;1、如果右侧是0，则左侧数保持不变&lt;/p&gt;
&lt;p&gt;2、如果右侧是1，则左侧数一定清零&lt;/p&gt;
&lt;p&gt;3、功能同a&amp;amp;(^b)相同&lt;/p&gt;
&lt;p&gt;4、如果左侧是变量，也等同于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a int
a &amp;amp;^= b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;^(XOR) 在Go语言中XOR是作为二元运算符存在的：&lt;/p&gt;
&lt;p&gt;但是如果是作为一元运算符出现，他的意思是按位取反。&lt;/p&gt;
&lt;p&gt;如果作为二元运算符则是, XOR是不进位加法计算，也就是异或计算。0000 0100 + 0000 0010 = 0000 0110 = 6&lt;/p&gt;
&lt;p&gt;常见可用于整数和浮点数的二元运算符有 +、-、* 和 /。
（相对于一般规则而言，Go 在进行字符串拼接时允许使用对运算符 + 的重载，但 Go 本身不允许开发者进行自定义的运算符重载）&lt;/p&gt;
&lt;p&gt;对于整数运算而言，结果依旧为整数，例如：9 / 4 -&amp;gt; 2。&lt;/p&gt;
&lt;p&gt;取余运算符只能作用于整数：9 % 4 -&amp;gt; 1。&lt;/p&gt;
&lt;p&gt;浮点数除以 0.0 会返回一个无穷尽的结果，使用 +Inf 表示。&lt;/p&gt;
&lt;p&gt;你可以将语句 b = b + a 简写为 b+=a，同样的写法也可用于 -=、*=、/=、%=。&lt;/p&gt;
&lt;p&gt;对于整数和浮点数，你可以使用一元运算符 ++（递增）和 --（递减），但只能用于后缀：&lt;/p&gt;
&lt;p&gt;i++ -&amp;gt; i += 1 -&amp;gt; i = i + 1&lt;/p&gt;
&lt;p&gt;i-- -&amp;gt; i -= 1 -&amp;gt; i = i - 1&lt;/p&gt;
&lt;p&gt;同时，带有 ++ 和 -- 的只能作为语句，而非表达式，因此 n = i++ 这种写法是无效的。&lt;/p&gt;
&lt;p&gt;函数 rand.Float32 和 rand.Float64 返回介于 [0.0，1.0) 之间的伪随机数，其中包括 0.0 但不包括 1.0。函数 rand.Intn 返回介于 [0，n) 之间的伪随机数。&lt;/p&gt;
&lt;p&gt;你可以使用 Seed(value) 函数来提供伪随机数的生成种子，一般情况下都会使用当前时间的纳秒级数字。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_08_project.md&quot;&gt;第八章 Go项目开发与编译&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_10_string.md&quot;&gt;第十章 string&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title># 《Go语言四十二章经》第八章 Go项目开发与编译</title><link>https://blog.wemang.com/posts/go/study/42_08_project/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_08_project/</guid><pubDate>Sun, 21 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;8.1 项目结构&lt;/h2&gt;
&lt;p&gt;Go的工程项目管理非常简单，使用目录结构和包名来确定工程结构和构建顺序。&lt;/p&gt;
&lt;p&gt;环境变量GOPATH在项目管理中非常重要，想要构建一个项目，必须确保项目目录在GOPATH中。而GOPATH可以有多个项目用&quot;;&quot;分隔。&lt;/p&gt;
&lt;p&gt;Go 项目目录下一般有三个子目录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;src存放源代码&lt;/li&gt;
&lt;li&gt;pkg编译后生成的文件&lt;/li&gt;
&lt;li&gt;bin编译后生成的可执行文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们重点要关注的其实就是src文件夹中的目录结构。&lt;/p&gt;
&lt;p&gt;为了进行一个项目，我们会在GOPATH目录下的src目录中，新建立一个项目的主要目录，比如我写的一个WEB项目《使用gin快速搭建WEB站点以及提供RESTful接口》。
https://github.com/ffhelicopter/tmm
项目主要目录“tmm”： GOPATH/src/github.com/ffhelicopter/tmm
在这个目录(tmm)下面还有其他目录，分别放置了其他代码，大概结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/github.com/ffhelicopter/tmm  
                               /api  
                               /handler
                               /model
                               /task
                               /website
                               main.go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;main.go 文件中定义了package main 。同时也在文件中import了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;github.com/ffhelicopter/tmm/api&quot;
&quot;github.com/ffhelicopter/tmm/handler&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2个自定义包。&lt;/p&gt;
&lt;p&gt;上面的目录结构是一般项目的目录结构，基本上可以满足单个项目开发的需要。如果需要构建多个项目，可按照类似的结构，分别建立不同项目目录。&lt;/p&gt;
&lt;p&gt;当我们运行go install main.go 会在GOPATH的bin 目录中生成可执行文件。&lt;/p&gt;
&lt;h2&gt;8.2 使用godoc&lt;/h2&gt;
&lt;p&gt;在程序中我们一般都会注释，如果我们按照一定规则，godoc工具会收集这些注释并产生一个技术文档。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Copyright 2009 The Go Authors. All rights reserved.  
// Use of this source code is governed by a BSD-style  
// license that can be found in the LICENSE file.     

package zlib
....

// A Writer takes data written to it and writes the compressed
// form of that data to an underlying writer (see NewWriter).
type Writer struct {
    w           io.Writer
    level       int
    dict        []byte
    compressor  * flate.Writer
    digest      hash.Hash32
    err         error
    scratch     [4]byte
    wroteHeader bool
}

// NewWriter creates a new Writer.
// Writes to the returned Writer are compressed and written to w.
//
// It is the caller&apos;s responsibility to call Close on the WriteCloser when done.
// Writes may be buffered and not flushed until Close.
func NewWriter(w io.Writer) * Writer {
    z, _ := NewWriterLevelDict(w, DefaultCompression, nil)
    return z
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令行下进入目录下并输入命令： godoc -http=:6060 -goroot=&quot;.&quot;&lt;/p&gt;
&lt;p&gt;然后在浏览器打开地址：http://localhost:6060&lt;/p&gt;
&lt;p&gt;然后你会看到本地的 Godoc 页面，从左到右一次显示出目录中的包。
&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/godoc.png&quot; alt=&quot;godoc.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;8.3 Go程序的编译&lt;/h2&gt;
&lt;p&gt;在Go语言中，和编译有关的命令主要是go run ,go build , go install这三个命令。&lt;/p&gt;
&lt;p&gt;go run只能作用于main包文件，先运行compile 命令编译生成.a文件，然后 link 生成最终可执行文件并运行程序，这过程的产生的是临时文件，在go run 退出前会删除这些临时文件（含.a文件和可执行文件）。最后直接在命令行输出程序执行结果。go run 命令在第二次执行的时候，如果发现导入的代码包没有发生变化，那么 go run 不会再次编译这个导入的代码包，直接进行链接生成最终可执行文件并运行程序。&lt;/p&gt;
&lt;p&gt;go install用于编译并安装指定的代码包及它们的依赖包，并且将编译后生成的可执行文件放到 bin 目录下（GOPATH/bin），编译后的包文件放到当前工作区的 pkg 的平台相关目录下。&lt;/p&gt;
&lt;p&gt;go build用于编译指定的代码包以及它们的依赖包。如果用来编译非main包的源码，则只做检查性的编译，而不会输出任何结果文件。如果是一个可执行程序的源码（即是 main 包），这个过程与go run 大体相同，除了会在当前目录生成一个可执行文件外。&lt;/p&gt;
&lt;p&gt;使用go build时有一个地方需要注意，对外发布编译文件如果不希望被人看到源代码，请使用go build -ldflags 命令，设置编译参数-ldflags &quot;-w -s&quot; 再编译后发布。避免使用gdb来调试而清楚看到源代码。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/ch5.png&quot; alt=&quot;ch5.png&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;8.4 Go modules 包依赖管理&lt;/h2&gt;
&lt;p&gt;Go 1.11 新增了对模块的支持，希望借此解决“包依赖管理”。可以通过设置环境变量 GO111MODULE来开启或关闭模块支持，它有三个可选值： off、 on、 auto，默认值是 auto。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GO111MODULE=off
无模块支持，go 会从 GOPATH 和 vendor 文件夹寻找包。&lt;/li&gt;
&lt;li&gt;GO111MODULE=on
模块支持，go 会忽略 GOPATH 和 vendor 文件夹，只根据 go.mod下载依赖。&lt;/li&gt;
&lt;li&gt;GO111MODULE=auto
在 GOPATH/src外面且根目录有 go.mod文件时，开启模块支持。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在使用模块的时候， GOPATH是无意义的，不过它还是会把下载的依赖储存在 GOPATH/pkg/mod 中。&lt;/p&gt;
&lt;p&gt;运行命令，go help mod ，我们可以看到mod的操作子命令，主要是init、 edit、 tidy。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Go mod provides access to operations on modules.

Note that support for modules is built into all the go commands,
not just &apos;go mod&apos;. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using &apos;go get&apos;.
See &apos;go help modules&apos; for an overview of module functionality.

Usage:

        go mod &amp;lt;command&amp;gt; [arguments]

The commands are:

        download    download modules to local cache
        edit        edit go.mod from tools or scripts
        graph       print module requirement graph
        init        initialize new module in current directory
        tidy        add missing and remove unused modules
        vendor      make vendored copy of dependencies
        verify      verify dependencies have expected content
        why         explain why packages or modules are needed

Use &quot;go help mod &amp;lt;command&amp;gt;&quot; for more information about a command.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令含义：
download   下载依赖的module到本地cache
edit        编辑go.mod文件
graph      打印模块依赖图
init        在当前文件夹下初始化一个新的module, 创建go.mod文件
tidy       增加丢失的module，去掉未用的module
vendor     将依赖复制到vendor下
verify      校验依赖
why       解释为什么需要依赖&lt;/p&gt;
&lt;p&gt;为了使用modules来管理项目，我们可以以下几个步骤来操作：&lt;/p&gt;
&lt;p&gt;（1）首先需要设置GO111MODULE ，这里我们设置为auto。&lt;/p&gt;
&lt;p&gt;（2）考虑和原来GOPATH有所隔离，新建立了一个目录D:\gomodules来存放modules管理的项目。&lt;/p&gt;
&lt;p&gt;（3）在D:\gomodules下建立ind项目，建立对应的目录，D:\gomodules\ind&lt;/p&gt;
&lt;p&gt;（4）在ind目录中，我们编写了该项目的主要文件main.go&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
package main

import (
	&quot;fmt&quot;
	&quot;github.com/gocolly/colly&quot;
)

func main() {
	c := colly.NewCollector()
	// Find and visit all links
	c.OnHTML(&quot;a[href]&quot;, func(e *colly.HTMLElement) {
		e.Request.Visit(e.Attr(&quot;href&quot;))
	})

	c.OnRequest(func(r *colly.Request) {
		fmt.Println(&quot;Visiting&quot;, r.URL)
	})
	c.Visit(&quot;http://go-colly.org/&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1、第一次需要我们运行init命令初始化：&lt;/p&gt;
&lt;p&gt;D:\gomodules\ind&amp;gt;go mod init ind&lt;/p&gt;
&lt;p&gt;go: creating new go.mod: module ind&lt;/p&gt;
&lt;p&gt;可以在ind目录看到新生成了一个文件：go.mod ，这个modules名字叫ind。&lt;/p&gt;
&lt;p&gt;2、接下来我们运行go mod tidy 命令，发现如下图一样出现报错，这主要是众所周知的网络原因，由于这里主要是golang.org/x下的包，所以可以简单使用replace命令来解决这个问题，如果是其他厂商的依赖包，还是优先解决网络问题。&lt;/p&gt;
&lt;p&gt;然后重复运行go mod tidy ，如果出错在使用replace，直到能正常运行go mod tidy 命令完成。&lt;/p&gt;
&lt;p&gt;go mod edit -replace=old[@v]=new[@v]&lt;/p&gt;
&lt;p&gt;注意：replace版本号可以在错误信息中看到。&lt;/p&gt;
&lt;p&gt;D:\gomodules\ind&amp;gt;go mod edit -replace=golang.org/x/net@v0.0.0-20181114220301-adae6a3d119a=github.com/golang/net@v0.0.0-20181114220301-adae6a3d119a&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/tidy.png&quot; alt=&quot;go mod tidy 命令&quot; /&gt;&lt;/p&gt;
&lt;p&gt;3、我们看到在ind目录下面多了2个文件，分别是go.mod和go.sum。&lt;/p&gt;
&lt;p&gt;go.mod文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module ind

replace (
	golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01 =&amp;gt; github.com/golang/net v0.0.0-20180218175443-cbe0f9307d01
	golang.org/x/net v0.0.0-20181114220301-adae6a3d119a =&amp;gt; github.com/golang/net v0.0.0-20181114220301-adae6a3d119a
)

require (
	github.com/PuerkitoBio/goquery v1.5.0 // indirect
	github.com/antchfx/htmlquery v0.0.0-20181207070731-9784ecda34b7 // indirect
	github.com/antchfx/xmlquery v0.0.0-20181204011708-431a9e9e7c44 // indirect
	github.com/antchfx/xpath v0.0.0-20181208024549-4bbdf6db12aa // indirect
	github.com/gobwas/glob v0.2.3 // indirect
	github.com/gocolly/colly v1.1.0
	github.com/kennygrant/sanitize v1.2.4 // indirect
	github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
	github.com/temoto/robotstxt v0.0.0-20180810133444-97ee4a9ee6ea // indirect
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;go.mod文件可以通过require，replace和exclude语句使用的精确软件包集。&lt;/p&gt;
&lt;p&gt;（1）require语句指定的依赖项模块&lt;/p&gt;
&lt;p&gt;（2）replace语句可以替换依赖项模块&lt;/p&gt;
&lt;p&gt;（3）exclude语句可以忽略依赖项模块&lt;/p&gt;
&lt;p&gt;go.sum文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antchfx/htmlquery v0.0.0-20181207070731-9784ecda34b7 h1:w7OFcAjjWOJ/Fp9/dlvikG46C44FV/B8G42Tj+KlFUk=
github.com/antchfx/htmlquery v0.0.0-20181207070731-9784ecda34b7/go.mod h1:MS9yksVSQXls00iXkiMqXr0J+umL/AmxXKuP28SUJM8=
github.com/antchfx/xmlquery v0.0.0-20181204011708-431a9e9e7c44 h1:utJNS82e0x9ZhwWvitDlUv2+0HgGYfyrSKX9hDf0uW0=
github.com/antchfx/xmlquery v0.0.0-20181204011708-431a9e9e7c44/go.mod h1:/+CnyD/DzHRnv2eRxrVbieRU/FIF6N0C+7oTtyUtCKk=
github.com/antchfx/xpath v0.0.0-20181208024549-4bbdf6db12aa h1:lL66YnJWy1tHlhjSx8fXnpgmv8kQVYnI4ilbYpNB6Zs=
github.com/antchfx/xpath v0.0.0-20181208024549-4bbdf6db12aa/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gocolly/colly v1.1.0 h1:B1M8NzjFpuhagut8f2ILUDlWMag+nTx+PWEmPy7RhrE=
github.com/gocolly/colly v1.1.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
github.com/golang/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:98y8FxUyMjTdJ5eOj/8vzuiVO14/dkJ98NYhEPG8QGY=
github.com/golang/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:98y8FxUyMjTdJ5eOj/8vzuiVO14/dkJ98NYhEPG8QGY=
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/temoto/robotstxt v0.0.0-20180810133444-97ee4a9ee6ea h1:hH8P1IiDpzRU6ZDbDh/RDnVuezi2oOXJpApa06M0zyI=
github.com/temoto/robotstxt v0.0.0-20180810133444-97ee4a9ee6ea/go.mod h1:aOux3gHPCftJ3KHq6Pz/AlDjYJ7Y+yKfm1gU/3B0u04=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开目录GOPATH/pkg/mod，我们可以看到这个项目下的依赖包都下载过来了。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_07_package.md&quot;&gt;第七章 代码结构化&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_09_operator.md&quot;&gt;第九章 运算符&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>《Go语言四十二章经》第七章 代码结构化</title><link>https://blog.wemang.com/posts/go/study/42_07_package/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_07_package/</guid><pubDate>Sat, 20 Jan 2024 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;《Go语言四十二章经》第七章 代码结构化&lt;/h1&gt;
&lt;h2&gt;7.1 包的概念&lt;/h2&gt;
&lt;p&gt;Go语言使用包（package）的概念来组织管理代码，包是结构化代码的一种方式。和其他语言如JAVA类似，Go语言中包的主要作用是把功能相似或相关的代码组织在同一个包中，以方便查找和使用。在Go语言中，每个.go文件都必须归属于某一个包，每个文件都可有init()函数。包名在源文件中第一行通过关键字package指定，包名要小写。如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package fmt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个目录下面可以有多个.go文件，这些文件只能属于同一个包，否则编译时会报错。同一个包下的不同.go文件相互之间可以直接引用变量和函数，所以这些文件中定义的全局变量和函数不能重名。&lt;/p&gt;
&lt;p&gt;Go语言的可执行应用程序必须有main包，而且在main包中必须且只能有一个main()函数，main()函数是应用程序运行开始入口。在main包中也可以使用init()函数。&lt;/p&gt;
&lt;p&gt;Go语言不强制要求包的名称和文件所在目录名称相同，但是这两者最好保持相同，否则很容易引起歧义。因为导入包的时候，会使用目录名作为包的路径，而在代码中使用时，却要使用包的名称。&lt;/p&gt;
&lt;h2&gt;7.2 包的导入&lt;/h2&gt;
&lt;p&gt;一个Go程序通过import关键字将一组包链接在一起。import其实是导入目录，而不是定义的包名称，实际应用中我们一般都会保持一致。&lt;/p&gt;
&lt;p&gt;例如标准包中定义的big包：package big，import  &quot;math/big&quot; ，源代码其实是在GOROOT下src中的src/math/big目录。在代码中使用big.Int时，big指的才是.go文件中定义的包名称。&lt;/p&gt;
&lt;p&gt;当导入多个包时，一般按照字母顺序排列包名称，像LiteIDE会在保存文件时自动完成这个动作。所谓导入包即等同于包含了这个包的所有的代码对象。&lt;/p&gt;
&lt;p&gt;为避免名称冲突，同一包中所有对象的标识符必须要求唯一。但是相同的标识符可以在不同的包中使用，因为可以使用包名来区分它们。&lt;/p&gt;
&lt;p&gt;import语句一般放在包名定义的下一行，导入包示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import  &quot;context&quot;  //加载context包
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导入多个包的常见的方式是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import  (
&quot;fmt&quot;
&quot;net/http&quot;
 )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用导入的包函数的一般方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fmt.Println(&quot;Hello World!&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面介绍三种特殊的import方式。&lt;/p&gt;
&lt;p&gt;点操作的含义是某个包导入之后，在调用这个包的函数时，可以省略前缀的包名，如这里可以写成Println(&quot;Hello World!&quot;)，而不是fmt.Println(&quot;Hello World!&quot;)。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import( . &quot;fmt&quot; ) 

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;别名操作就是可以把包命名成另一个容易记忆的名字。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import(
    f &quot;fmt&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;别名操作调用包函数时，前缀变成了别名，即f.Println(&quot;Hello World!&quot;)。在实际项目中有时这样使用，但请谨慎使用，不要不加节制地采用这种形式。&lt;/p&gt;
&lt;p&gt;_ 操作是引入某个包，但不直接使用包里的函数，而是调用该包里面的init函数，比如下面的mysql包的导入。此外在开发中，由于某种原因某个原来导入的包现在不再使用，也可以采用这种方式处理，比如下面fmt的包。代码示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
	_ &quot;fmt&quot;
	_ &quot;github.com/go-sql-driver/mysql&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7.3 标准库&lt;/h2&gt;
&lt;p&gt;在 Go 的安装文件里包含了一些可以直接使用的标准库。在GOROOT/src中可以看到源码，也可以根据情况自行重新编译。&lt;/p&gt;
&lt;p&gt;完整列表可以访问GoWalker（https://gowalker.org/）查看。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    unsafe: 包含了一些打破 Go 语言“类型安全”的命令，一般的程序中不会被使用，可用在 C/C++ 程序的调用中。
    syscall-os-os/exec:
    	os: 提供给我们一个平台无关性的操作系统功能接口，采用类UNIX设计，隐藏了不同操作系统间差异，让不同的文件系统和操作系统对象表现一致。
    	os/exec: 提供我们运行外部操作系统命令和程序的方式。
    	syscall: 底层的外部包，提供了操作系统底层调用的基本接口。
    archive/tar 和 /zip-compress：压缩(解压缩)文件功能。
    fmt-io-bufio-path/filepath-flag:
    	fmt: 提供了格式化输入输出功能。
    	io: 提供了基本输入输出功能，大多数是围绕系统功能的封装。
    	bufio: 缓冲输入输出功能的封装。
    	path/filepath: 用来操作在当前系统中的目标文件名路径。
    	flag: 对命令行参数的操作。　　
    strings-strconv-unicode-regexp-bytes:
    	strings: 提供对字符串的操作。
    	strconv: 提供将字符串转换为基础类型的功能。
    	unicode: 为 unicode 型的字符串提供特殊的功能。
    	regexp: 正则表达式功能。
    	bytes: 提供对字符型分片的操作。
    math-math/cmath-math/big-math/rand-sort:
    	math: 基本的数学函数。
    	math/cmath: 对复数的操作。
    	math/rand: 伪随机数生成。
    	sort: 为数组排序和自定义集合。
    	math/big: 大数的实现和计算。 　　
    container-/list-ring-heap: 实现对集合的操作。
    	list: 双链表。
    	ring: 环形链表。
   time-log:
        time: 日期和时间的基本操作。
        log: 记录程序运行时产生的日志。
    encoding/Json-encoding/xml-text/template:
        encoding/Json: 读取并解码和写入并编码 Json 数据。
        encoding/xml:简单的 XML1.0 解析器。
        text/template:生成像 HTML 一样的数据与文本混合的数据驱动模板。
    net-net/http-html:
        net: 网络数据的基本操作。
        http: 提供了一个可扩展的 HTTP 服务器和客户端，解析 HTTP 请求和回复。
        html: HTML5 解析器。
    runtime: Go 程序运行时的交互操作，例如垃圾回收和协程创建。
    reflect: 实现通过程序运行时反射，让程序操作任意类型的变量。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;7.4 从 GitHub 安装包&lt;/h2&gt;
&lt;p&gt;如果有人想安装您的远端项目到本地机器，打开终端并执行（ffhelicopter是我在GitHub上的用户名）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go get -u github.com/ffhelicopter/tmm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样现在这台机器上的其他 Go 应用程序也可以通过导入路径：&quot;github.com/ffhelicopter/tmm&quot; 来使用。 开发中一般这样操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &quot;github.com/ffhelicopter/tmm&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go 对包的版本管理做的不是很友好，不过现在有些第三方项目做的不错，有兴趣的同学可以了解下（glide、godep、govendor）。&lt;/p&gt;
&lt;h2&gt;7.5 导入外部安装包&lt;/h2&gt;
&lt;p&gt;如果你要在你的应用中使用一个或多个外部包，你可以使用go install在你的本地机器上安装它们。go install 是Go语言中自动包安装工具：如需要将包安装到本地它会从远端仓库下载包：检出、编译和安装一气呵成。&lt;/p&gt;
&lt;p&gt;在包安装前的先决条件是要自动处理包自身依赖关系的安装。被依赖的包也会安装到子目录下，但是没有文档和示例：可以到网上浏览。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;go install 使用了 GOPATH 变量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设你想使用https://github.com/gocolly/colly 这种托管在 Google Code、GitHub 和 Launchpad 等代码网站上的包。&lt;/p&gt;
&lt;p&gt;你可以通过如下命令安装： go install github.com/gocolly/colly 将一个名为 github.com/gocolly/colly   安装在GOPATH/pkg/ 目录下。&lt;/p&gt;
&lt;p&gt;go install/build都是用来编译包和其依赖的包。&lt;/p&gt;
&lt;p&gt;区别： go build只对main包有效，在当前目录编译生成一个可执行的二进制文件（依赖包生成的静态库文件放在GOPATH/pkg）。&lt;/p&gt;
&lt;p&gt;go install一般生成静态库文件放在GOPATH/pkg目录下，文件扩展名a。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果为main包，运行Go build则会在GOPATH/bin 生成一个可执行的二进制文件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;7.6 包的初始化&lt;/h2&gt;
&lt;p&gt;可执行应用程序的初始化和执行都起始于main包。如果main包的源代码中没有包含main()函数，则会引发构建错误 undefined: main.main。main()函数既没有参数，也没有返回类型，init()函数和main()函数在这一点上两者一样。&lt;/p&gt;
&lt;p&gt;如果main包还导入了其它的包，那么就会在编译时将它们依次导入。有时某个包会被多个包同时导入，那么它只会被导入一次（例如很多包可能都会用到fmt包，但它只会被导入一次，因为没有必要导入多次）。&lt;/p&gt;
&lt;p&gt;当某个包被导入时，如果该包还导入了其它的包，那么会先将其它包导入进来，然后再对这些包中的包级常量和变量进行初始化，接着执行init()函数（如果有的话），依次类推。&lt;/p&gt;
&lt;p&gt;等所有被导入的包都加载完毕了，就会开始对main包中的包级常量和变量进行初始化，然后执行main包中的init()函数，最后执行main()函数。&lt;/p&gt;
&lt;p&gt;Go语言中init()函数常用于包的初始化，该函数是Go语言的一个重要特性，有下面的特征：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;init函数是用于程序执行前做包的初始化的函数，比如初始化包里的变量等&lt;/li&gt;
&lt;li&gt;每个包可以拥有多个init函数&lt;/li&gt;
&lt;li&gt;包的每个源文件也可以拥有多个init函数&lt;/li&gt;
&lt;li&gt;同一个包中多个init()函数的执行顺序不定&lt;/li&gt;
&lt;li&gt;不同包的init()函数按照包导入的依赖关系决定该函数的执行顺序&lt;/li&gt;
&lt;li&gt;init()函数不能被其他函数调用，其在main函数执行之前，自动被调用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_06_convention.md&quot;&gt;第六章 约定和惯例&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_08_project.md&quot;&gt;第八章 Go项目开发与编译&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>《Go语言四十二章经》第六章 约定和惯例</title><link>https://blog.wemang.com/posts/go/study/42_06_convention/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_06_convention/</guid><pubDate>Fri, 19 Jan 2024 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;《Go语言四十二章经》第六章 约定和惯例&lt;/h1&gt;
&lt;h2&gt;6.1 可见性规则&lt;/h2&gt;
&lt;p&gt;在Go语言中，标识符必须以一个大写字母开头，这样才可以被外部包的代码所使用，这被称为导出。标识符如果以小写字母开头，则对包外是不可见的，但是他们在整个包的内部是可见并且可用的。但是包名不管在什么情况下都必须小写。&lt;/p&gt;
&lt;p&gt;在设计Go语言时，设计者们也希望确保它不是过于以ASCII为中心，这意味着需要从7位ASCII的范围来扩展标识符的空间。 所以Go语言标识符规定必须是Unicode定义的字母或数字，标识符是一个或多个Unicode字母和数字的序列， 标识符中的第一个字符必须是Unicode字母。&lt;/p&gt;
&lt;p&gt;这条规则还有另外一个不幸的后果。由于导出的标识符必须以大写字母开头，因此根据定义，从某些语言的字符创建的标识符不能导出。目前唯一的解决方案是使用像“A语言”这样的东西，但这显然不能令人满意。&lt;/p&gt;
&lt;p&gt;总而言之，为了确保我们的标识符能正常导出，我们建议在开发中还是尽量使用ASCII 码来作为标识符，虽然设计者们在避免以ASCII 码为中心，但出于习惯我们还是服从于这个现实。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;那么问题来了，使用中文命名的标识符能够正常导出吗？希望大家在了解后面的知识后，可以尝试一下试试。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;6.2 命名规范以及语法惯例&lt;/h2&gt;
&lt;p&gt;当某个函数需要被外部包调用的时候需要使用大写字母开头，并遵循 Pascal 命名法（“大驼峰式命名法”）；否则就遵循“小驼峰式命名法”，即第一个单词的首字母小写，其余单词的首字母大写。&lt;/p&gt;
&lt;p&gt;单词之间不以空格断开或连接号（-）、底线（_）连结，第一个单词首字母采用大写字母；后续单词的首字母亦用大写字母，例如：FirstName、LastName。每一个单词的首字母都采用大写字母的命名格式，被称为“Pascal命名法”，源自于Pascal语言的命名惯例，也有人称之为“大驼峰式命名法”（Upper Camel Case），为驼峰式大小写的子集。&lt;/p&gt;
&lt;p&gt;当二个或二个以上单词连结在一起时，用驼峰式命名法可以增加变量和函数名称的可读性。&lt;/p&gt;
&lt;p&gt;Go 语言追求简洁的代码风格，并通过 gofmt 强制实现风格统一。&lt;/p&gt;
&lt;p&gt;Go 语言也使用分号作为语句的结束，但一般会省略分号。像在标识符后面；整数、浮点、复数、Rune或字符串等字面量后面；关键字break、continue、fallthrough、或者return后面；操作符或标点符号++、--、)、]或}之后等等都可以使用分号，但是往往会省略掉，像LiteIDE编辑器会在保存.go文件时自动过滤掉这些分号，所以在Go语言开发中一般不用过多关注分号的使用。&lt;/p&gt;
&lt;p&gt;左大括号 &lt;code&gt;{&lt;/code&gt; 不能单独一行，这是编译器的强制规定，否则你在使用 gofmt 时就会出现错误提示“ &lt;code&gt;expected declaration, found &apos;{&apos; &lt;/code&gt;”。右大括号 &lt;code&gt;}&lt;/code&gt; 需要单独一行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func functionName) () {
   …
}

if mod &amp;gt; 0 {
	div++
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在定义接口名时也有惯例，一般单方法接口由方法名称加上-er后缀来命名。&lt;/p&gt;
&lt;h2&gt;6.3 注释&lt;/h2&gt;
&lt;p&gt;在Go语言中，注释有两种形式：&lt;/p&gt;
&lt;p&gt;1.行注释：使用双斜线//开始，一般后面紧跟一个空格。行注释是Go语言中最常见的注释形式，在标准包中，一般都采用行注释，建议采用这种方式。
2.块注释：使用 /* */，块注释不能嵌套。块注释一般用于包描述或注释成块的代码片段。&lt;/p&gt;
&lt;p&gt;一般而言，注释文字尽量每行长度接近一致，过长的行应该换行以方便在编辑器阅读。注释可以是单行，多行，甚至可以使用doc.go文件来专门保存包注释。每个包只需要在一个go文件的package关键字上面注释，两者之间没有空行。对于变量，函数，结构体，接口等的注释直接加在声明前，注释与声明之间没有空行。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:generate go run genzfunc.go

// Package sort provides primitives for sorting slices and user-defined
// collections.
package sort

// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
	// Len is the number of elements in the collection.
	Len() int
	// Less reports whether the element with
	// index i should sort before the element with index j.
	Less(i, j int) bool
	// Swap swaps the elements with indexes i and j.
	Swap(i, j int)
}

// Insertion sort
func insertionSort(data Interface, a, b int) {
	for i := a + 1; i &amp;lt; b; i++ {
		for j := i; j &amp;gt; a &amp;amp;&amp;amp; data.Less(j, j-1); j-- {
			data.Swap(j, j-1)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数或方法的注释需要以函数名开始，且两者之间没有空行，示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ContainsRune reports whether the rune is contained in the UTF-8-encoded byte slice b.
func ContainsRune(b []byte, r rune) bool {
	return IndexRune(b, r) &amp;gt;= 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要预格式化的部分，直接加空格缩进即可，示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// For example, flags Ldate | Ltime (or LstdFlags) produce,
//	2009/01/23 01:23:23 message
// while flags Ldate | Ltime | Lmicroseconds | Llongfile produce,
//	2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在方法，结构体或者包注释前面加上“Deprecated:”表示不建议使用，示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Deprecated: Old 老旧方法，不建议使用
func Old(a int)(int){
    return a
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在注释中，还可以插入空行，示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Search calls f(i) only for i in the range [0, n).
//
// A common use of Search is to find the index i for a value x in
// a sorted, indexable data structure such as an array or slice.
// In this case, the argument f, typically a closure, captures the value
// to be searched for, and how the data structure is indexed and
// ordered.
//
// For instance, given a slice data sorted in ascending order,
// the call Search(len(data), func(i int) bool { return data[i] &amp;gt;= 23 })
// returns the smallest index i such that data[i] &amp;gt;= 23. If the caller
// wants to find whether 23 is in the slice, it must test data[i] == 23
// separately.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_05_scope.md&quot;&gt;第五章 作用域&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_07_package.md&quot;&gt;第七章 代码结构化&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>《Go语言四十二章经》第五章 作用域</title><link>https://blog.wemang.com/posts/go/study/42_05_scope/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_05_scope/</guid><pubDate>Thu, 18 Jan 2024 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;《Go语言四十二章经》第五章 作用域&lt;/h1&gt;
&lt;h2&gt;5.1 作用域&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;局部变量
在函数体内或代码块内声明的变量称之为局部变量，它们的作用域只在代码块内，参数和返回值变量也是局部变量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全局变量
作用域都是全局的（在本包范围内）
在函数体外声明的变量称之为全局变量，全局变量可以在整个包甚至外部包（被导出后）使用。
全局变量可以在任何函数中使用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;简式变量
使用 := 定义的变量，如果新变量Ga与那个同名已定义变量 (这里就是那个全局变量Ga)不在一个作用域中时，那么Go 语言会新定义这个变量Ga，遮盖住全局变量Ga。刚开始很容易在此犯错而茫然，解决方法是局部变量尽量不同名。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据 Go语言的规范 ，Go的标识符作用域是基于代码块（code block）的。代码块就是包裹在一对大括号内部的声明和语句，并且是可嵌套的。在代码中直观可见的显式的(explicit)code block，比如：函数的函数体、for循环的循环体等；还有隐式的(implicit)code block。&lt;/p&gt;
&lt;p&gt;我们使用最多的if语句类型就是 单if型 ，即:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if simplestmt; expression {
    ... ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种类型的if语句中，有两个code block：一个隐式的code block和一个显式的code block。我们把上面的形式代码做一个等价变化，并加上code block起始和结束点的标注，结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ // 隐式code block
    simplestmt
    if expression { // 显式的code block
            ... ...
    } 
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面的代码综合了几种作用域的情况，很容易混淆。请各位仔细琢磨弄清楚。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

var (
	Ga int = 99
)

const (
	v int = 199
)

func GetGa() func() int {

	if Ga := 55; Ga &amp;lt; 60 {
		fmt.Println(&quot;GetGa if 中：&quot;, Ga)
	}

	for Ga := 2; ; {
		fmt.Println(&quot;GetGa循环中：&quot;, Ga)
		break
	}

	fmt.Println(&quot;GetGa函数中：&quot;, Ga)

	return func() int {
		Ga += 1
		return Ga
	}
}

func main() {
	Ga := &quot;string&quot;
	fmt.Println(&quot;main函数中：&quot;, Ga)

	b := GetGa()
	fmt.Println(&quot;main函数中：&quot;, b(), b(), b(), b())

	v := 1
	{
		v := 2
		fmt.Println(v)
		{
			v := 3
			fmt.Println(v)
		}
	}
	fmt.Println(v)
}


&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出：

main函数中： string
GetGa if 中： 55
GetGa循环中： 2
GetGa函数中： 99
main函数中： 100 101 102 103
2
3
1

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ga作为全局变量存在是int类型，值为99；而在main()中时，Ga通过简式声明 := 操作，是string类型，值为string。在main()中，v很典型地体现了在“{}”花括号中的作用域问题，每一层花括号，都是对上一层的屏蔽。而闭包函数，GetGa()返回的匿名函数，赋值给b，每次执行b()，Ga的值都被记忆在内存中，下次执行b()的时候，取b()上次执行后Ga的值，而不是全局变量Ga的值，这就是闭包函数可以使用包含它的函数内的变量，因为作为代码块一直存在，所以每次执行都是在上次基础上运行。&lt;/p&gt;
&lt;p&gt;简单总结如下：&lt;/p&gt;
&lt;p&gt;有花括号&quot;{ }&quot;一般都存在作用域的划分；
:= 简式声明会屏蔽所有上层代码块中的变量（常量），建议使用规则来规范，如对常量使用全部大写，而变量尽量小写；
在if等语句中存在隐式代码块，需要注意；
闭包函数可以理解为一个代码块，并且他可使用包含它的函数内的变量；&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意，简式变量只能在函数内部声明使用，但是它可能会覆盖函数外全局同名变量。而且你不能在一个单独的声明中重复声明一个变量，但在多变量声明中这是允许的，而且其中至少要有一个新的声明变量。重复变量需要在相同的代码块内，否则你将得到一个隐藏变量。&lt;/p&gt;
&lt;p&gt;如果你在代码块中犯了这个错误，将不会出现编译错误，但应用运行结果可能不是你所期望。所以尽可能避免和全局变量同名。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;思考：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    if a := 1; false {
    } else if b := 2; false {
    } else if c := 3; false {
    } else {
        println(a, b, c)
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码运行结果是什么，你能写出来吗？&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_04_const.md&quot;&gt;第四章 常量&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_06_convention.md&quot;&gt;第六章 约定和惯例&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>《Go语言四十二章经》第四章 常量</title><link>https://blog.wemang.com/posts/go/study/42_04_const/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_04_const/</guid><pubDate>Wed, 17 Jan 2024 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;《Go语言四十二章经》第四章 常量&lt;/h1&gt;
&lt;h2&gt;4.1 常量以及iota&lt;/h2&gt;
&lt;p&gt;常量使用关键字 const 定义，用于存储不会改变的数据。常量不能被重新赋予任何值。
存储在常量中的数据类型只可以是布尔型、数字型（整数型、浮点型和复数）和字符串型。
常量的定义格式：const identifier [type] = value，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const Pi = 3.14159
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Go 语言中，你可以省略类型说明符 [type]，因为编译器可以根据变量（常量）的值来推断其类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;显式类型定义： const b string = &quot;abc&quot;
隐式类型定义： const b = &quot;abc&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go的常量定义可以限定常量类型，但不是必需的。如果定义常量时没有指定类型，那么它与字面常量一样，是无类型（untyped）常量。一个没有指定类型的常量被使用时，会根据其使用环境而推断出它所需要具备的类型。换句话说，未定义类型的常量会在必要时刻根据上下文来获得相关类型。&lt;/p&gt;
&lt;p&gt;字面常量（literal），是指程序中硬编码的常量，如：-12。它们的值即为它们本身，是无法被改变的。&lt;/p&gt;
&lt;p&gt;常量的值必须是能够在编译时就能够确定的；你可以在其赋值表达式中涉及计算过程，但是所有用于计算的值必须在编译期间就能获得。&lt;/p&gt;
&lt;p&gt;Go语言预定义了这些常量： true、 false和iota。布尔常量只包含两个值：true 和 false。iota比较特殊，可以被认为是一个可被编译器修改的常量，在每一个const关键字出现时被重置为0，然后在下一个const出现之前，每出现一次iota，其所代表的数字会自动增1。&lt;/p&gt;
&lt;p&gt;在这个例子中，iota 可以被用作枚举值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const (
    a = iota
    b = iota
    c = iota
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个 iota 等于 0，每当 iota 在新的一行被使用时，它的值都会自动加 1；所以 a=0, b=1, c=2 可以简写为如下形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const (
    a = iota
    b
    c
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const (
    a = iota
    b = 8
    c
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;a, b, c分别为0, 8, 8，新的常量b声明后，iota 不再向下赋值，后面常量如果没有赋值，则继承上一个常量值。&lt;/p&gt;
&lt;p&gt;可以简单理解为在一个const块中，每换一行定义个常量，iota 都会自动+1。&lt;/p&gt;
&lt;p&gt;（ 关于 iota 的使用涉及到非常复杂多样的情况 ，这里不展开来讲了，有兴趣可以查查资料研究）&lt;/p&gt;
&lt;p&gt;iota 也可以用在表达式中，如：iota + 50。在每遇到一个新的常量块或单个常量声明时， iota 都会重置为 0（ **简单地讲，每遇到一次 const 关键字，iota 就重置为 0 ** ）。&lt;/p&gt;
&lt;p&gt;使用位左移与 iota 计数配合可优雅地实现存储单位的常量枚举：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ByteSize float64
const (
    _ = iota // 通过赋值给空白标识符来忽略值
    KB ByteSize = 1&amp;lt;&amp;lt;(10*iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数值常量（Numeric constants）包括整数，浮点数以及复数常量。数值常量有一些微妙之处。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

func main() {
	const a = 5
	var intVar int = a
	var int32Var int32 = a
	var float64Var float64 = a
	var complex64Var complex64 = a
	fmt.Println(&quot;intVar&quot;, intVar, &quot;\nint32Var&quot;, int32Var, &quot;\nfloat64Var&quot;, float64Var, &quot;\ncomplex64Var&quot;, complex64Var)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;程序输出
intVar 5 
int32Var 5 
float64Var 5 
complex64Var (5+0i)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个程序中，a 的值是 5 并且 a 在语法上是泛化的（它既可以表示浮点数 5.0，也可以表示整数 5，甚至可以表示没有虚部的复数 5 + 0i），因此 a 可以赋值给任何与之类型兼容的变量。像 a 这种数值常量的默认类型可以想象成是通过上下文动态生成的。&lt;/p&gt;
&lt;p&gt;当然，常量之所以为常量就是恒定不变的量，因此我们无法在程序运行过程中修改它的值；如果你在代码中试图修改常量的值则会引发编译错误。同时，在const 定义中，对常量名没有强制要求全部大写，不过我们一般都会全部字母大写，以便阅读。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_03_var.md&quot;&gt;第三章 变量&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_05_scope.md&quot;&gt;第五章 作用域&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>《Go语言四十二章经》第三章 变量</title><link>https://blog.wemang.com/posts/go/study/42_03_var/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_03_var/</guid><pubDate>Tue, 16 Jan 2024 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;《Go语言四十二章经》第三章 变量&lt;/h1&gt;
&lt;h2&gt;3.1 变量以及声明&lt;/h2&gt;
&lt;p&gt;Go 语言中有四类标记：标识符(identifiers)，关键字(keywords)，运算符(operators )和标点符号(punctuation)以及字面量(literals) 。&lt;/p&gt;
&lt;p&gt;Go 语言变量标识符由字母、数字、下划线组成，其中首个字母不能为数字，同一字母的大小写在Go语言中代表不同标识，注意区分A 和a 是不同的标识。&lt;/p&gt;
&lt;p&gt;根据Go语言规范，标识符命名程序实体，例如变量和类型。 标识符是一个或多个Unicode字母和数字的序列。 标识符中的第一个字符必须是Unicode字母。标识符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;identifier = letter { letter | unicode_digit } .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Go语言规范中，下划线“_”也被认为是字母：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The underscore character _ (U+005F) is considered a letter.
letter        = unicode_letter | &quot;_&quot; .
unicode_digit  = /* a Unicode code point classified as &quot;Number, decimal digit&quot; */ .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Unicode标准8.0中，第4.5节“常规类别”定义了一组字符类别。 Go语言将Unicode中任何字母类别Lu，Ll，Lt，Lm或Lo中的所有字符视为Unicode字母，将数字类别Nd中的字符视为Unicode数字。&lt;/p&gt;
&lt;p&gt;据统计，Go语言视为Unicode的字母（含下划线_）一共20871个，这里面包括中文，详情见表：&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;Lu&lt;/td&gt;
&lt;td&gt;字母,大写&lt;/td&gt;
&lt;td&gt;1781&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ll&lt;/td&gt;
&lt;td&gt;字母,小写&lt;/td&gt;
&lt;td&gt;2145&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lt&lt;/td&gt;
&lt;td&gt;字母,词首字母大写&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lm&lt;/td&gt;
&lt;td&gt;字母,修饰符&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lo&lt;/td&gt;
&lt;td&gt;字母,其他&lt;/td&gt;
&lt;td&gt;16053&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nd&lt;/td&gt;
&lt;td&gt;数字,十进制数&lt;/td&gt;
&lt;td&gt;610&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一般习惯上，在Go语言命名标识符时，我们还是选择英文的52个大小写字母以及0-9数字和下划线来组合成合适的标识符。上表中其他的字符也可以用于标识符，但不在上表中的字符是不能用在Go语言标识符中。后面我们提到大写字母，主要是指Lu类别中的1781个字母。&lt;/p&gt;
&lt;p&gt;另外，Go语言中关键字是保留字，不能作为变量标识符，如下表所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;break&lt;/th&gt;
&lt;th&gt;default&lt;/th&gt;
&lt;th&gt;func&lt;/th&gt;
&lt;th&gt;interface&lt;/th&gt;
&lt;th&gt;select&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;case&lt;/td&gt;
&lt;td&gt;defer&lt;/td&gt;
&lt;td&gt;go&lt;/td&gt;
&lt;td&gt;map&lt;/td&gt;
&lt;td&gt;struct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;chan&lt;/td&gt;
&lt;td&gt;else&lt;/td&gt;
&lt;td&gt;goto&lt;/td&gt;
&lt;td&gt;package&lt;/td&gt;
&lt;td&gt;switch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;const&lt;/td&gt;
&lt;td&gt;fallthrough&lt;/td&gt;
&lt;td&gt;if&lt;/td&gt;
&lt;td&gt;range&lt;/td&gt;
&lt;td&gt;type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;continue&lt;/td&gt;
&lt;td&gt;for&lt;/td&gt;
&lt;td&gt;import&lt;/td&gt;
&lt;td&gt;return&lt;/td&gt;
&lt;td&gt;var&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Go语言变量声明使用关键字var，下面我们声明了几个变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var (
    a int
    b bool
    str string
    浮点 float32    // 没错，中文可以作为变量标识符
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种因式分解关键字的写法一般用于声明全局变量，一般在func 外定义。&lt;/p&gt;
&lt;p&gt;当一个变量被var声明之后，系统自动赋予它该类型的零值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;int   为 0&lt;/li&gt;
&lt;li&gt;float  为 0.0&lt;/li&gt;
&lt;li&gt;bool  为 false&lt;/li&gt;
&lt;li&gt;string 为空字符串&quot;&quot;&lt;/li&gt;
&lt;li&gt;指针为 nil&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;记住，这些变量在 Go 中都是经过初始化的。&lt;/p&gt;
&lt;p&gt;多变量可以在同一行进行赋值，也称为 并行 或 同时 或 平行赋值。如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a, b, c = 5, 7, &quot;abc&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简式声明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a, b, c := 5, 7, &quot;abc&quot;  // 注意等号前的冒号
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;右边的这些值以相同的顺序赋值给左边的变量，所以 a 的值是 5， b 的值是 7，c 的值是 &quot;abc&quot;。&lt;/p&gt;
&lt;p&gt;简式声明一般用在func内，要注意的是：全局变量和简式声明的变量尽量不要同名，否则很容易产生偶然的变量隐藏Accidental Variable Shadowing。&lt;/p&gt;
&lt;p&gt;即使对于经验丰富的Go开发者而言，这也是一个非常常见的陷阱。这个坑很容易挖，但又很难发现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {  
    x := 1
    fmt.Println(x)     // prints 1
    {
        fmt.Println(x) // prints 1
        x := 2
        fmt.Println(x) // prints 2
    }
    fmt.Println(x)     // prints 1 (不是2)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想要交换两个变量的值，则可以简单地使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; a, b = b, a  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(在 Go 语言中，这样省去了使用交换函数的必要)&lt;/p&gt;
&lt;p&gt;空白标识符 _ 也被用于抛弃值，如值 5 在：&lt;code&gt;_, b = 5, 7&lt;/code&gt; 中被抛弃。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_, b = 5, 7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;_ 实际上是一个只写变量，你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量，但有时你并不需要使用从一个函数得到的所有返回值。&lt;/p&gt;
&lt;p&gt;由于Go语言有个强制规定，在函数内一定要使用声明的变量，但未使用的全局变量是没问题的。为了避免有未使用的变量，代码将编译失败，我们可以将该未使用的变量改为 _。&lt;/p&gt;
&lt;p&gt;另外，在Go语言中，如果引入的包未使用，也不能通过编译。有时我们需要引入的包，比如需要init()，或者调试代码时我们可能去掉了某些包的功能使用，你可以添加一个下划线标记符，_，来作为这个包的名字，从而避免编译失败。下滑线标记符用于引入，但不使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

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

var _ = log.Println
func main() {  
    _ = time.Now
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并行赋值也被用于当一个函数返回多个返回值时，比如这里的 val 和错误 err 是通过调用 Func1 函数同时得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val, err = Func1(var1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于布尔值的好的命名能够很好地提升代码的可读性，例如以 is 或者 Is 开头的 isSorted、isFinished、isVisible，使用这样的命名能够在阅读代码的获得阅读正常语句一样的良好体验，例如标准库中的 unicode.IsDigit(ch)。&lt;/p&gt;
&lt;p&gt;在 Go 语言中，指针属于引用类型，其它的引用类型还包括 slices，maps和 channel。&lt;/p&gt;
&lt;p&gt;注意，Go中的数组是数值，因此当你向函数中传递数组时，函数会得到原始数组数据的一份复制。如果你打算更新数组的数据，可以考虑使用数组指针类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {  
    x := [3]int{1, 2, 3}

    func(arr *[3]int) {
        (*arr)[0] = 7
        fmt.Println(arr) // prints &amp;amp;[7 2 3]
    }(&amp;amp;x)

    fmt.Println(x) // prints [7 2 3]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;被引用的变量会存储在堆中，以便进行垃圾回收，且比栈拥有更大的内存空间。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;引申：&lt;/p&gt;
&lt;p&gt;编译器会做逃逸分析，所以由Go的编译器决定在哪(堆or栈)分配内存，保证程序的正确性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;3.2 零值nil&lt;/h2&gt;
&lt;p&gt;nil 标志符用于表示interface、函数、maps、slices、channels、error、指针等的“零值”。如果你不指定变量的类型，编译器将无法编译你的代码，因为它猜不出具体的类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

func main() {  
    var x = nil // 错误

    _ = x
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在一个 nil 的slice中添加元素是没问题的，但对一个map做同样的事将会生成一个运行时的panic：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

func main() {  
    var m map[string]int
    m[&quot;one&quot;] = 1 //error

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字符串不会为 nil&lt;/p&gt;
&lt;p&gt;这对于经常使用 nil 分配字符串变量的开发者而言是个需要注意的地方。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var str string  = &quot;&quot; // &quot;&quot;是字符串的零值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据前面的介绍，其实这样写和上面的效果一样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var str string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_02_datatype.md&quot;&gt;第二章 数据类型&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_04_const.md&quot;&gt;第四章 常量&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>《Go语言四十二章经》第二章 数据类型</title><link>https://blog.wemang.com/posts/go/study/42_02_datatype/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_02_datatype/</guid><pubDate>Mon, 15 Jan 2024 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;《Go语言四十二章经》第二章 数据类型&lt;/h1&gt;
&lt;p&gt;在 Go 语言中，数据类型可用于参数和变量声明。&lt;/p&gt;
&lt;h2&gt;2.1 基本数据类型&lt;/h2&gt;
&lt;p&gt;Go 语言按类别有以下几种数据类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;布尔型：
布尔型的值只可以是常量 true 或者 false。一个简单的例子：&lt;code&gt;var b bool = true&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数字类型：
整型 int 和浮点型 float32、float64，Go 语言支持整型和浮点型数字，并且原生支持复数，其中位的运算采用补码。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;字符串类型：&amp;gt;
字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识Unicode文本。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;派生类型：
包括：&lt;/p&gt;
&lt;p&gt;(a) 指针类型（Pointer）
(b) 数组类型
(c) 结构类型(struct)
(d) Channel 类型
(e) 函数类型
(f) 切片类型
(g) 接口类型（interface）
(h) Map 类型&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数字类型：&lt;/h3&gt;
&lt;p&gt;Go 也有基于架构的类型，例如：int、uint 和 uintptr，这些类型的长度都是根据运行程序所在的操作系统类型所决定的。&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;uint8&lt;/td&gt;
&lt;td&gt;无符号&lt;/td&gt;
&lt;td&gt;8位整型 (0 到 255)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uint16&lt;/td&gt;
&lt;td&gt;无符号&lt;/td&gt;
&lt;td&gt;16位整型 (0 到 65535)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uint32&lt;/td&gt;
&lt;td&gt;无符号&lt;/td&gt;
&lt;td&gt;32位整型 (0 到 4294967295)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uint64&lt;/td&gt;
&lt;td&gt;无符号&lt;/td&gt;
&lt;td&gt;64位整型 (0 到 18446744073709551615)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int8&lt;/td&gt;
&lt;td&gt;有符号&lt;/td&gt;
&lt;td&gt;8位整型 (-128 到 127)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int16&lt;/td&gt;
&lt;td&gt;有符号&lt;/td&gt;
&lt;td&gt;16位整型 (-32768 到 32767)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int32&lt;/td&gt;
&lt;td&gt;有符号&lt;/td&gt;
&lt;td&gt;32位整型 (-2147483648 到 2147483647)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int64&lt;/td&gt;
&lt;td&gt;有符号&lt;/td&gt;
&lt;td&gt;64位整型 (-9223372036854775808 到 9223372036854775807)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;浮点型：&lt;/h2&gt;
&lt;p&gt;主要是为了表示小数，也可细分为float32和float64两种。浮点数能够表示的范围可以从很小到很巨大，这个极限值范围可以在math包中获取，math.MaxFloat32表示float32的最大值，大约是3.4e38，math.MaxFloat64大约是1.8e308，两个类型最小的非负值大约是1.4e-45和4.9e-324。&lt;/p&gt;
&lt;p&gt;float32大约可以提供小数点后6位的精度，作为对比，float64可以提供小数点后15位的精度。通常情况应该优先选择float64，因此float32的精确度较低，在累积计算时误差扩散很快，而且float32能精确表达的最小正整数并不大，因为浮点数和整数的底层解释方式完全不同。&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;float32&lt;/td&gt;
&lt;td&gt;IEEE-754   32位浮点型数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;float64&lt;/td&gt;
&lt;td&gt;IEEE-754   64位浮点型数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;其他数字类型：&lt;/h2&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;byte&lt;/td&gt;
&lt;td&gt;类似 uint8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rune&lt;/td&gt;
&lt;td&gt;类似 int32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uint32&lt;/td&gt;
&lt;td&gt;或 64 位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;与 uint 一样大小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uintptr&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;只读的Unicode字节序列，Go语言使用UTF-8格式编码Unicode字符，每个字符对应一个rune类型。一旦字符串变量赋值之后，内部的字符就不能修改，英文是一个字节，中文是三个字节。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;string转int：    int, err := strconv.Atoi(string)
string转int64：  int64, err := strconv.ParseInt(string, 10, 64)
int转string：    string := strconv.Itoa(int)
int64转string：  string := strconv.FormatInt(int64, 10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而一个range循环会在每次迭代时，解码一个UTF-8编码的符文。每次循环时，循环的索引是当前文字的起始位置，以字节为单位，代码点是它的值（rune）。&lt;/p&gt;
&lt;p&gt;使用range迭代字符串时，需要注意的是range迭代的是Unicode而不是字节。返回的两个值，第一个是被迭代的字符的UTF-8编码的第一个字节在字符串中的索引，第二个值的为对应的字符且类型为rune(实际就是表示unicode值的整形数据）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const s = &quot;Go语言&quot;
for i, r := range s {
	fmt.Printf(&quot;%#U  ： %d\n&quot;, r, i)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;程序输出：&lt;/p&gt;
&lt;p&gt;U+0047 &apos;G&apos;   ： 0
U+006F &apos;o&apos;   ： 1
U+8BED &apos;语&apos;  ： 2
U+8A00 &apos;言&apos;  ： 5&lt;/p&gt;
&lt;h2&gt;复数：&lt;/h2&gt;
&lt;p&gt;复数类型相对用的很少，主要是数学学科专业会用上。分为两种类型 complex64和complex128 前部分是实体后部分是虚体。&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;complex64&lt;/td&gt;
&lt;td&gt;32位实数和虚数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;complex128&lt;/td&gt;
&lt;td&gt;64位实数和虚数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;2.2 Unicode（UTF-8）&lt;/h2&gt;
&lt;p&gt;你可以通过增加前缀 0 来表示 8 进制数（如：077），增加前缀 0x 来表示 16 进制数（如：0xFF），以及使用 e 来表示 10 的连乘（如： 1e3 = 1000，或者 6.022e23 = 6.022 x 1e23）&lt;/p&gt;
&lt;p&gt;不过 Go 同样支持 Unicode（UTF-8），因此字符同样称为 Unicode 代码点或者 runes，并在内存中使用 int 来表示。在文档中，一般使用格式 U+hhhh 来表示，其中 h 表示一个 16 进制数。其实 rune 也是 Go 当中的一个类型，并且是 int32 的别名。&lt;/p&gt;
&lt;p&gt;在书写 Unicode 字符时，需要在 16 进制数之前加上前缀 \u 或者 \U。&lt;/p&gt;
&lt;p&gt;因为 Unicode 至少占用 2 个字节，所以我们使用 int16 或者 int 类型来表示。如果需要使用到 4 字节，则会加上 \U 前缀；前缀 \u 则总是紧跟着长度为 4 的 16 进制数，前缀 \U 紧跟着长度为 8 的 16 进制数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var ch int = &apos;\u0041&apos;
var ch2 int = &apos;\u03B2&apos;
var ch3 int = &apos;\U00101234&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.3 复数&lt;/h2&gt;
&lt;p&gt;Go 拥有以下复数类型：&lt;/p&gt;
&lt;p&gt;complex64 (32 位实数和虚数)
complex128 (64 位实数和虚数)&lt;/p&gt;
&lt;p&gt;复数使用 re+imi 来表示，其中 re 代表实数部分，im 代表虚数部分，i 为虚数单位。
示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var c1 complex64 = 5 + 10i
fmt.Printf(&quot;The value is: %v&quot;, c1)// 输出： 5 + 10i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 re 和 im 的类型均为 float32，那么类型为 complex64 的复数 c 可以通过以下方式来获得：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c = complex(re, im)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数 real(c) 和 imag(c) 可以分别获得相应的实数和虚数部分。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_01_install.md&quot;&gt;第一章 Go安装与运行&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_03_var.md&quot;&gt;第三章 变量&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>《Go语言四十二章经》第一章 Go安装与运行</title><link>https://blog.wemang.com/posts/go/study/42_01_install/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/study/42_01_install/</guid><pubDate>Sun, 14 Jan 2024 10:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;《Go语言四十二章经》第一章 Go安装与运行&lt;/h1&gt;
&lt;p&gt;Go语言是一门全新的静态类型开发语言，具有自动垃圾回收，丰富的内置类型, 函数多返回值，错误处理，匿名函数, 并发编程，反射，defer等关键特征，并具有简洁、安全、并行、开源等特性。从语言层面支持并发，可以充分的利用CPU多核，Go语言编译的程序可以媲美C或C++代码的速度，而且更加安全、支持并行进程。系统标准库功能完备，尤其是强大的网络库让建立Web服务成为再简单不过的事情。简单易学，内置runtime，支持继承、对象等，开发工具丰富，例如gofmt工具，自动格式化代码，让团队代码风格完美统一。同时Go非常适合用来进行服务器编程，网络编程，包括Web应用、API应用，分布式编程等等。&lt;/p&gt;
&lt;p&gt;“Go让我体验到了从未有过的开发效率。”谷歌资深工程师罗布·派克(Rob Pike)如是说道，和C++或C一样，Go是一种系统语言，他表示，“使用它可以进行快速开发，同时它还是一个真正的编译语言，我们之所以现在将其开源，原因是我们认为它已经非常有用和强大。”&lt;/p&gt;
&lt;p&gt;Go语言自2009年面世以来，已经有越来越多的公司开始转向Go语言开发，比如腾讯、百度、阿里、京东、小米以及360，而七牛云其技术栈基本上完全采用Go语言来开发。还有像今日头条、UBER这样的公司，他们也使用Go语言对自己的业务进行了彻底的重构。在全球范围内Go语言的使用不断增长，尤其是在云计算领域，用Go语言编写的几个主要云基础项目如Docker和Kubernetes，都取得了巨大成功。除此之外，还有各种有名的项目如etcd/consul/flannel等等，均使用Go语言实现。&lt;/p&gt;
&lt;p&gt;Go语言有两快，一是编译运行快，还有一个是学习上手快。Go语言的学习曲线并不陡峭，无论是刚开始接触编程的朋友，还是有其他语言开发经验而打算学习Go语言的朋友，大家都可以放心大胆来学习和了解Go语言，“它值得拥有！”&lt;/p&gt;
&lt;p&gt;让我们开始Go语言学习之旅吧！&lt;/p&gt;
&lt;h2&gt;&amp;lt;a id=&quot;11-go安装&quot;&amp;gt;1.1 Go安装&amp;lt;/a&amp;gt;&lt;/h2&gt;
&lt;p&gt;要用Go语言来进行开发，需要先搭建开发环境。Go 语言支持以下系统：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux&lt;/li&gt;
&lt;li&gt;FreeBSD&lt;/li&gt;
&lt;li&gt;Mac OS&lt;/li&gt;
&lt;li&gt;Windows&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先需要下载Go语言安装包，Go语言的安装包下载地址为：https://golang.org/dl/ ， 国内可以正常下载地址：https://golang.google.cn/dl/&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;源码编译安装&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Go语言是谷歌2009发布的第二款开源编程语言。经过几年的版本更迭，目前Go已经发布1.11版本，UNIX/Linux/Mac OS X，和 FreeBSD系统下可使用如下源码安装方法：&lt;/p&gt;
&lt;p&gt;（1）下载源码包：https://golang.google.cn/dl/go1.11.1.linux-amd64.tar.gz
（2）将下载的源码包解压至 /usr/local目录：
tar -C /usr/local -xzf go1.11.1.linux-amd64.tar.gz
（3）将 /usr/local/go/bin 目录添加至PATH环境变量：
export PATH=$PATH:/usr/local/go/bin
（4）设置GOPATH，GOROOT环境变量：
GOPATH是工作目录，GOROOT为Go的安装目录，这里为/usr/local/go/&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：MAC系统下你可以使用 .pkg 结尾的安装包直接双击来完成安装，安装目录在 /usr/local/go/ 下。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Windows系统下安装&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们在Windows系统下一般采用直接安装，下载go1.11.1.windows-amd64.zip版本，直接解压到安装目录D:\Go，然后把D:\Go\bin目录添加到 PATH 环境变量中。&lt;/p&gt;
&lt;p&gt;另外，还需要设置2个重要环境变量：&lt;/p&gt;
&lt;p&gt;GOPATH=D:\goproject
GOROOT=D:\Go\&lt;/p&gt;
&lt;p&gt;以上三个环境变量设置好后，我们就可以开始正式使用Go语言来开发了。&lt;/p&gt;
&lt;p&gt;Windows系统也可以选择go1.11.1.windows-amd64.msi，双击运行程序根据提示来操作。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GOPATH是我们的工作目录，可以有多个，用分号隔开。
GOROOT为Go的安装目录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Win+R打开CMD（注意：设置环境变量后需要重新打开CMD），输入 go ，如下显示说明Go语言运行环境已经安装成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;D:\goproject\src&amp;gt;go
Go is a tool for managing Go source code.

Usage:

        go &amp;lt;command&amp;gt; [arguments]

The commands are:

        bug         start a bug report
        build       compile packages and dependencies
        clean       remove object files and cached files
        doc         show documentation for package or symbol
        env         print Go environment information
        fix         update packages to use new APIs
        fmt         gofmt (reformat) package sources
        generate    generate Go files by processing source
        get         download and install packages and dependencies
        install     compile and install packages and dependencies
        list        list packages or modules
        mod         module maintenance
        run         compile and run Go program
        test        test packages
        tool        run specified go tool
        version     print Go version
        vet         report likely mistakes in packages

Use &quot;go help &amp;lt;command&amp;gt;&quot; for more information about a command.

Additional help topics:

        buildmode   build modes
        c           calling between Go and C
        cache       build and test caching
        environment environment variables
        filetype    file types
        go.mod      the go.mod file
        gopath      GOPATH environment variable
        gopath-get  legacy GOPATH go get
        goproxy     module proxy protocol
        importpath  import path syntax
        modules     modules, module versions, and more
        module-get  module-aware go get
        packages    package lists and patterns
        testflag    testing flags
        testfunc    testing functions

Use &quot;go help &amp;lt;topic&amp;gt;&quot; for more information about that topic.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，我们输入go version，可看到我们安装的Go版本，如图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/img/gv.png&quot; alt=&quot;gotool.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在本书中，所有代码编译运行和标准库的说明讲解都基于go1.11，还没有升级的用户请及时升级。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GOPATH允许多个目录，当有多个目录时，请注意分隔符，多个目录的时候Windows是分号;&lt;/p&gt;
&lt;p&gt;当有多个GOPATH时默认将go get获取的包存放在第一个目录下。&lt;/p&gt;
&lt;p&gt;GOPATH目录约定有三个子目录&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;src存放源代码(比如：.go .c .h .s等)   按照Go 默认约定，go run，go install等命令的当前工作路径（即在此路径下执行上述命令）。&lt;/li&gt;
&lt;li&gt;pkg编译时生成的中间文件（比如：.a）&lt;/li&gt;
&lt;li&gt;bin编译后生成的可执行文件，接下来就可以试试代码编译运行了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文件名: test.go，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
   fmt.Println(&quot;Hello, World!&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用go命令执行以上代码输出结果如下：&lt;/p&gt;
&lt;p&gt;D:\goproject&amp;gt;go run test.go&lt;/p&gt;
&lt;p&gt;Hello，World!&lt;/p&gt;
&lt;h2&gt;1.2 Go语言开发工具&lt;/h2&gt;
&lt;p&gt;LiteIDE是一款开源、跨平台的轻量级 Go 语言集成开发环境（IDE）。在安装LiteIDE之前一定要先安装Go语言环境。LiteIDE支持以下的操作系统：
Windows x86 (32-bit or 64-bit)
Linux x86 (32-bit or 64-bit)&lt;/p&gt;
&lt;p&gt;LiteIDE可以通过以下途径下载：&lt;/p&gt;
&lt;p&gt;下载地址：https://sourceforge.net/projects/liteide/files/&lt;/p&gt;
&lt;p&gt;源码地址：https://github.com/visualfc/liteide&lt;/p&gt;
&lt;p&gt;golang中国：https://www.golangtc.com/download/liteide&lt;/p&gt;
&lt;p&gt;也提供下载，国内下载速度可能会快一些，但版本更新较慢，建议还是选择官方地址下载。&lt;/p&gt;
&lt;p&gt;Windows直接安装：&lt;/p&gt;
&lt;p&gt;Windows下选择 liteidex35.1.windows-qt5.9.5.zip，下载之后解压，在liteide\bin文件夹下找到liteide.exe，双击运行。&lt;/p&gt;
&lt;p&gt;如果不出意外，将会出现LiteIDE的运行界面。&lt;/p&gt;
&lt;p&gt;有关LiteIDE 的使用相对来说比较简单，很容易上手，就不在此细说了。&lt;/p&gt;
&lt;p&gt;源码编译安装：&lt;/p&gt;
&lt;p&gt;LiteIDE源码位于https://github.com/visualfc/liteide上。需要使用Qt4/Qt5来编译源代码，Qt库可以从https://qt-project.org/downloads上获取。Mac OS X用户可以不从源代码编译Qt，直接在终端中运行brew update &amp;amp;&amp;amp; brew install qt，节省大量时间。&lt;/p&gt;
&lt;p&gt;有关LiteIDE 安装的更多说明请访问： http://liteide.org/cn/doc/install/&lt;/p&gt;
&lt;p&gt;其他的开发工具还有Eclipse以及其集成goeclipse开发插件，以及Sublime text等，可以根据个人喜好情况选择使用。&lt;/p&gt;
&lt;p&gt;现在Go 语言和开发工具我们都已经安装完成，接下来我们开始学习Go的基础知识，并实际使用他们来进行练习和开发。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/SUMMARY.md&quot;&gt;目录&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/README.md&quot;&gt;前言&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_01_install.md&quot;&gt;第一章 Go安装与运行&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ffhelicopter/Go42/blob/master/content/42_02_datatype.md&quot;&gt;第二章 数据类型&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本书《Go语言四十二章经》内容在github上同步地址：https://github.com/ffhelicopter/Go42&lt;/p&gt;
&lt;p&gt;虽然本书中例子都经过实际运行，但难免出现错误和不足之处，烦请您指出；如有建议也欢迎交流。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>21 React扩展阅读</title><link>https://blog.wemang.com/posts/web/react/react%E6%89%A9%E5%B1%95%E9%98%85%E8%AF%BB/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/react%E6%89%A9%E5%B1%95%E9%98%85%E8%AF%BB/</guid><pubDate>Tue, 19 Sep 2023 08:58:27 GMT</pubDate><content:encoded>&lt;h1&gt;React扩展阅读&lt;/h1&gt;
&lt;h4&gt;Youtube中Codevolution专栏下的React Hooks Tutorial系列视频&lt;/h4&gt;
&lt;p&gt;链接：&lt;a href=&quot;https://pan.baidu.com/s/1Lj_kN-FuO5bbZ2rqMVz6xw&quot;&gt;https://pan.baidu.com/s/1Lj_kN-FuO5bbZ2rqMVz6xw&lt;/a&gt;&lt;br /&gt;
提取码：70ni&lt;/p&gt;
&lt;h4&gt;自定义 Hook 大全&lt;/h4&gt;
&lt;p&gt;必不可少的 React Hooks集合。&lt;br /&gt;
&lt;a href=&quot;https://github.com/zenghongtu/react-use-chinese&quot;&gt;https://github.com/zenghongtu/react-use-chinese&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;React技术揭秘&lt;/h4&gt;
&lt;p&gt;卡颂写的 React 源码分析。&lt;br /&gt;
&lt;a href=&quot;https://react.iamkasong.com/&quot;&gt;https://react.iamkasong.com/&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;阿里 ahooks 工具库&lt;/h4&gt;
&lt;p&gt;蚂蚁umi团队、淘系ice团队、阿里体育共同建设的 React Hooks 工具库&lt;br /&gt;
&lt;a href=&quot;https://ahooks.js.org/zh-CN&quot;&gt;https://ahooks.js.org/zh-CN&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;网易云音乐前端团队 React Hooks 最佳实践&lt;/h4&gt;
&lt;p&gt;实际项目中总结出的 React Hooks 实用经验&lt;br /&gt;
&lt;a href=&quot;https://mp.weixin.qq.com/s/HwlnvAh18saKwXC_nZwSHw&quot;&gt;https://mp.weixin.qq.com/s/HwlnvAh18saKwXC_nZwSHw&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;React 300问&lt;/h4&gt;
&lt;p&gt;React 300多道面试题和答案&lt;br /&gt;
&lt;a href=&quot;https://github.com/semlinker/reactjs-interview-questions&quot;&gt;https://github.com/semlinker/reactjs-interview-questions&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;阿里推出的React框架：antd&lt;/h4&gt;
&lt;p&gt;antd 4.0版本全部采用函数组件开发而成。&lt;br /&gt;
&lt;a href=&quot;https://ant.design/docs/react/introduce-cn&quot;&gt;https://ant.design/docs/react/introduce-cn&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;京东推出的多端统一开发框架：taro&lt;/h4&gt;
&lt;p&gt;Taro是一套遵循React语法规范的多端开发解决方案。&lt;br /&gt;
&lt;a href=&quot;https://taro-docs.jd.com/taro/&quot;&gt;https://taro-docs.jd.com/taro/&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;学习React的几个好的微信公众号&lt;/h4&gt;
&lt;p&gt;React中文社区、不知非攻、魔术师卡颂&lt;/p&gt;
</content:encoded></item><item><title>20 React基础知识</title><link>https://blog.wemang.com/posts/web/react/react%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/react%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/</guid><pubDate>Tue, 19 Sep 2023 08:57:56 GMT</pubDate><content:encoded>&lt;h1&gt;React基础知识&lt;/h1&gt;
&lt;p&gt;说明：以下这些基础知识适用于类组件和函数组件，并不是函数组件独有的。&lt;/p&gt;
&lt;h2&gt;安装react并初始化&lt;/h2&gt;
&lt;h5&gt;1、安装：npm install -g create-react-app&lt;/h5&gt;
&lt;h5&gt;2、创建hello-react目录并初始化：npx create-react-app hello-react&lt;/h5&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;目录名不允许有大写字母&lt;/li&gt;
&lt;li&gt;初始化过程比较慢，甚至可能需要5-10分钟&lt;/li&gt;
&lt;li&gt;如果报错：npm ERR! Unexpected end of JSON input while parsing near &apos;...n\r\nwsFcBAEBCAAQBQJd&apos;， 解决方法：npm root -g 找到本机npm全局安装目录，cd 进入该目录，执行清除缓存：npm cache clean --force，然后再次初始化。&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;3、启动项目：cd hello-react、npm start&lt;/h5&gt;
&lt;p&gt;默认将启动：http://localhost:3000&lt;/p&gt;
&lt;h2&gt;自定义组件基础知识&lt;/h2&gt;
&lt;p&gt;1、自定义组件必须以大写字母开头、默认网页原生标签还以小写开头。请注意这里表述的&quot;默认网页原生标签&quot;本质上并不是真实的原生网页标签，他们是react默认定义好的、内置的自定义组件标签，只不过这些标签刚好和原生标签的作用，功能，名称一模一样而已。&lt;/p&gt;
&lt;p&gt;2、自定义组件如果不希望设定最外层的标签，那么可以使用react(16+版本)提供的占位符Fragment来充当最外层标签；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{Component,Fragment} from &apos;react&apos;;  
    类组件：render(){return &amp;lt;Fragment&amp;gt;xxxxxxx&amp;lt;/Fragment&amp;gt;}
    函数组件：return &amp;lt;Fragment&amp;gt;xxxxxxx&amp;lt;/Fragment&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在最新的react版本中，也可以直接使用&lt;code&gt;&amp;lt;&amp;gt;&amp;lt;/&amp;gt;&lt;/code&gt;来代替Fragment。其中&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;唯一可以拥有的属性为key。即&lt;code&gt;&amp;lt;key=&apos;xxx&apos;&amp;gt;&amp;lt;/&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;3、使用数组map循环更新li，一定要给li添加对应的key值，否则虽然正常运行，但是会报错误警告。不建议直接使用index作为key值。&lt;/p&gt;
&lt;p&gt;4、在最新的react版本中，为了提高更新性能，推荐采用异步的方式更新数据。具体使用方式为：&lt;code&gt;setXxx((prevData) =&amp;gt; {return xxx})&lt;/code&gt;。其中参数prevData指之前的变量值，return的对象指修改之后的数据值。&lt;/p&gt;
&lt;p&gt;可以将上面代码简写为：&lt;code&gt;setXxx(prevData =&amp;gt; xxx)&lt;/code&gt;
若没有用到prevData参数，还可以省略，即 &lt;code&gt;setXxx(() =&amp;gt; xxx)&lt;/code&gt;;&lt;/p&gt;
&lt;p&gt;异步的目的是为了优化更新性能，react短期内发现多条数据变量发生修改，那么他会将所有修改合并成一次修改再最终执行。&lt;/p&gt;
&lt;p&gt;5、在JSX中写注释，格式为：&lt;code&gt;{/* xxxxx */}&lt;/code&gt;或&lt;code&gt;{//xxxx}&lt;/code&gt;，注意如果使用单行注释，最外的大括号必须单独占一行。注释尽在开发源代码中显示，在导出的网页中不会有该注释。&lt;/p&gt;
&lt;p&gt;6、给标签添加样式时，推荐使用className，不推荐使用class。如果使用class虽然运行没问题，但是会报错误警告，因为样式class这个关键词和js中声明类的class冲突。类似的还有标签中for关键词，推荐改为htmlFor。&lt;/p&gt;
&lt;p&gt;7、通常情况下，react是针对组件开发，并且只负责对html中某一个div进行渲染，那么意味着该html其他标签不受影响，这样引申出来一个结果：一个html既可以使用react，也可以使用vue，两者可以并存。&lt;/p&gt;
&lt;p&gt;8、为了方便调试代码，可以在谷歌浏览器中安装React Developer Tools插件。安装后可在谷歌浏览器调试模式下，查看component标签下的内容。  若访问本机react调试网页则该插件图标为红色、若访问导出版本的React网页则该插线显示为蓝色、若访问的网页没使用react框架则为灰色。&lt;/p&gt;
&lt;p&gt;9、给组件设定属性，只有属性名没有属性值，那么默认react会将该属性值设置为true。在ES6中如果只有一个属性对象没有属性值，通常理解为该属性名和属性值是相同的。 为了避免混淆，不建议不给属性不设置属性值。&lt;/p&gt;
&lt;p&gt;10、ReactDOM.createPortal()用来将元素渲染到任意DOM元素中(包括顶级组件之外的其他DOM中)。&lt;/p&gt;
&lt;h2&gt;&quot;纯函数&quot; 概念解释&lt;/h2&gt;
&lt;p&gt;JS中定义的所有函数都可以增加参数，所谓&quot;纯函数&quot;是指函数内部并未修改过该参数的函数。&lt;/p&gt;
&lt;p&gt;例如以下函数：&lt;code&gt;function myFun(a){let c=a }&lt;/code&gt;，该函数内部从未更改过参数a，那么这个函数就是纯函数。&lt;/p&gt;
&lt;p&gt;反例，非纯函数 例如：&lt;code&gt;function myFun(a){a=a+2; let c=a}&lt;/code&gt;，该函数内部修改过参数a，那么这个函数就不再是纯函数了。&lt;/p&gt;
&lt;p&gt;纯函数的特殊意义是什么？&lt;br /&gt;
因为纯函数内部从不会直接修改参数，那么无论运行多少次，执行结果永远是一致的。&lt;/p&gt;
&lt;p&gt;若仅仅有一个函数，那么也无所谓，但是如果有多个函数都是都需要调用执行同一个变量(参数)，为了确保多个函数执行结果是符合预期的，那么就要求每个函数都不能在自己内部修改该变量(参数)。&lt;/p&gt;
&lt;p&gt;这就是为什么react不允许直接修改某变量的原因。&lt;/p&gt;
&lt;h2&gt;&quot;受控组件&quot; 概念解释&lt;/h2&gt;
&lt;p&gt;像input、select、textarea、form等将自身value与某变量进行绑定的组件，称之为受控组件。&lt;/p&gt;
&lt;p&gt;&quot;受控&quot;即这些组件的可以值受到某变量的控制。&lt;/p&gt;
&lt;p&gt;与之对应的是&quot;非受控组件&quot;，即该组件对应的值并不能被某变量控制。&lt;/p&gt;
&lt;p&gt;例如&quot;&lt;code&gt;&amp;lt;input type=&apos;file&apos;/&amp;gt;&lt;/code&gt;&quot;，该组件的值为用户选中本地的文件信息，该值并不能直接通过某变量来进行value值的设定，因此该组件属于&quot;非受控组件&quot;。&lt;/p&gt;
&lt;h2&gt;&quot;声明式开发&quot; 概念解释&lt;/h2&gt;
&lt;p&gt;&quot;声明式开发&quot;：基于数据定义和数据改变，视图层自动更新。&lt;br /&gt;
&quot;命令式开发&quot;：基于具体执行命令更改视图，例如DOM操作修改。&lt;/p&gt;
&lt;p&gt;注意：声明式开发并不是不进行DOM操作，而是把DOM操作频率降到最低。&lt;/p&gt;
&lt;h2&gt;&quot;单项数据流&quot; 概念解释&lt;/h2&gt;
&lt;p&gt;react框架的原则中规定，子组件只可以使用父组件传递过来的xxx属性对应的值或方法，不可以改变。&lt;/p&gt;
&lt;p&gt;数据只能单向发生传递(父传向子，不允许子直接修改父)，若子组件想修改父组件中的数据，只能通过父组件暴露给子组件的函数(方法)来间接修改。&lt;/p&gt;
&lt;p&gt;react框架具体实现方式是设置父组件传递给子组件的&quot;数据值或方法&quot;仅仅为可读，但不可修改。&lt;/p&gt;
&lt;p&gt;为什么要做这样的限制？&lt;br /&gt;
因为一个父组件可以有多个子组件，如果每个子组件都可修改父组件中的数据(子组件之间彼此共用父组件的数据)，一个子组件的数据修改会造成其他子组件数据更改，最终会让整个组件数据变得非常复杂。&lt;/p&gt;
&lt;p&gt;为了简化数据操作复杂程度，因此采用单向数据流策略，保证父组件数据的唯一最终可修改权归父组件所有。&lt;/p&gt;
&lt;h2&gt;&quot;视图层渲染框架&quot; 概念解释&lt;/h2&gt;
&lt;p&gt;react框架自身定位是&quot;视图层渲染框架&quot;，单向数据流概念很好，但是实际项目中页面会很复杂。&lt;/p&gt;
&lt;p&gt;例如顶级组件Root中分别使用了组件A(由子组件A0、A1、A2构成)、组件B(由子组件A0、A1、A2构成)、组件C(由子组件C0、C1、C2构成)，若此时组件A的子组件A2想和组件C的子组件C1进行数据交互，那么按照单向数据流的规范，数据操作流程为 A2 -&amp;gt; A -&amp;gt; Root -&amp;gt; C - C1，可以看出操作流程非常复杂。&lt;/p&gt;
&lt;p&gt;所以实际开发中，React框架也许会结合其他&quot;数据层框架&quot;(例如Redux、Flux等)，但是请注意，只从有了hook以后，可以通过useReducer+useContext来实现类似Redux的功能。&lt;/p&gt;
&lt;h2&gt;&quot;函数式编程&quot; 概念解释&lt;/h2&gt;
&lt;p&gt;react自定义组件的各种交互都在内部定义不同的函数(js语法规定：类class中定义的函数不需要在前面写 function关键词)，因此成为函数式编程。不像原生JS和html交互那样，更多侧重html标签、DOM操作来实现视图和交互。&lt;/p&gt;
&lt;p&gt;函数式编程的几点好处：&lt;br /&gt;
1、可以把复杂功能的处理函数拆分成多个细小的函数。&lt;br /&gt;
2、由于都是通过函数来进行视图层渲染和数据交互，更加方便编写&quot;前端自动化测试&quot;代码。&lt;/p&gt;
&lt;h2&gt;&quot;虚拟DOM&quot; 概念解释&lt;/h2&gt;
&lt;p&gt;虚拟DOM(Virtual Dom)就是一个JS对象(数组对象)，用来描述真实DOM。相对通过html标签创建的真实DOM，虚拟DOM是保存在客户端内存里的一份JS表述DOM的数组对象。&lt;/p&gt;
&lt;p&gt;用最简单的一个div标签来示意两者的差异，数据格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //真实DOM数据格式(网页标签)
    &amp;lt;div id=&apos;mydiv&apos;&amp;gt;hell react&amp;lt;/div&amp;gt;

    //虚拟DOM数据格式(JS数组对象)
    //虚拟DOM数组对象格式为：标签名+属性集合+值
    [&apos;div&apos;,{id:&apos;mydiv&apos;},&apos;hell react&apos;]
    
    //在JSX的创建模板代码中，通常代码格式为
    render(){return &amp;lt;div id=&apos;mydiv&apos;&amp;gt;hello react&amp;lt;/&amp;gt;}

    //还可以使用react提供的，更加底层的方法来实现
    render(){return React.createElement(&apos;div&apos;,{id:&apos;mydiv&apos;},&apos;hello react&apos;)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虚拟DOM更新性能快的原因并不是因为在内存中(理论上任何软件都是运行在内存中)，而是因为虚拟DOM储存的数据格式为JS对象，用JS来操作(生成/查询/对比/更新)JS对象很容易。用JS操作(生成/查询/对比/更新)真实DOM则需要调用Web Action层的API，性能相对就慢。&lt;/p&gt;
&lt;p&gt;react运行(更新)步骤，大致为：&lt;br /&gt;
1、定义组件数据变量&lt;br /&gt;
2、定义组件模板JSX&lt;br /&gt;
3、数据与模板结合，生成一份虚拟DOM&lt;br /&gt;
4、将虚拟DOM转化为真实DOM&lt;br /&gt;
5、将得到的真实DOM挂载到html中(通过真实DOM操作)，用来显示&lt;br /&gt;
6、监听变量发生改变，若有改变重新执行第3步(数据与模板结合，生成另外一份新的虚拟DOM)&lt;br /&gt;
7、在内存中对比前后两份虚拟DOM，找出差异部分(diff算法)&lt;br /&gt;
8、将差异部分转化为真实的DOM&lt;br /&gt;
8、将差异化的真实DOM，通过真实DOM操作进行更新&lt;/p&gt;
&lt;p&gt;当变量发生更改时，虚拟DOM减少了真实DOM的创建和对比次数(通过虚拟DOM而非真实DOM)，从而提高了性能。&lt;/p&gt;
&lt;h2&gt;&quot;Diff算法&quot; 概念解释&lt;/h2&gt;
&lt;p&gt;当变量发生改变时，需要重新生成新的虚拟DOM，并且对旧的虚拟DOM进行差异化比对。&lt;br /&gt;
Diff算法就是这个差异化比对的算法。&lt;/p&gt;
&lt;p&gt;Diff算法为了提高性能，优化算法，通常原则为：&lt;/p&gt;
&lt;h5&gt;同层(同级)虚拟DOM比对&lt;/h5&gt;
&lt;p&gt;先从两个虚拟DOM(JS对象)同层(即顶层)开始比对，如果发现同层就不一致，那么就直接放弃下一层(级别)的对比，采用最新的虚拟DOM。&lt;/p&gt;
&lt;p&gt;疑问点：假如两心虚拟DOM顶层不一致，但下一级别以及后面的更多级别都一致，如果仅仅因为顶层不一致而就该放弃下一级别，重新操作真实DOM从头渲染，岂不是性能浪费？&lt;/p&gt;
&lt;p&gt;答：同层(同级)虚拟DOM比对，&quot;比对&quot;算法相对简单，比对速度快。如果采用多层(多级)比对，&quot;比对&quot;算法会相对复杂，比对速度慢。 同层虚拟DOM比对就是利用了比对速度快的优势来抵消&quot;操作真实DOM操作性能上的浪费&quot;。&lt;/p&gt;
&lt;h5&gt;列表元素使用key值进行比对&lt;/h5&gt;
&lt;p&gt;这里的key值是值&quot;稳定的key值(是有规律的字符串，非数字)&quot;，若key值为索引数字index，那么顺序发生改变时，索引数字也会发生变化，无法判断之前的和现在的是否是同一个对象。&lt;/p&gt;
&lt;p&gt;如果key值是稳定的，那么在比对的时候，比较容易比对出是否发生变化，以及具体的变化是什么。&lt;/p&gt;
&lt;p&gt;Diff算法还有非常多的其他性能优化算法，以上列出的&quot;同层比对、key值比对&quot;仅仅为算法举例。&lt;/p&gt;
&lt;h2&gt;&quot;高阶组件&quot; 概念解释&lt;/h2&gt;
&lt;p&gt;高阶组件是一种组件设计方式(设计模式)，就是将一个组件作为参数传递给一个函数，该函数接收参数(组件)后进行处理和装饰，并返回出一个新的组件。&lt;/p&gt;
&lt;p&gt;简单来说就是，普通组件是根据参数(props)生成一个UI(JSX语法支持的标签)。而高阶组件是根据参数(组件)生成一个新的组件。&lt;/p&gt;
&lt;h2&gt;&quot;生命周期函数&quot; 概念解释&lt;/h2&gt;
&lt;p&gt;生命周期函数指在某一时刻组件会自动调用执行的函数。&lt;/p&gt;
&lt;p&gt;这里的&quot;某一时刻&quot;可以是指组件初始化、挂载到虚拟DOM、数据更改引发的更新(重新渲染)、从虚拟DOM卸载这4个阶段。&lt;/p&gt;
&lt;h4&gt;生命周期4个阶段和该阶段内的生命周期函数：&lt;/h4&gt;
&lt;h5&gt;初始化(Initialization)&lt;/h5&gt;
&lt;p&gt;constructor()是JS中原生类的构造函数，理论上他不专属于组件的初始化，但是如果把它归类成组件组初始化也是可以接受的。&lt;/p&gt;
&lt;h5&gt;挂载(Mounting)&lt;/h5&gt;
&lt;p&gt;componentWillMount(即将被挂载)、render(挂载)、componentDidMount(挂载完成)&lt;/p&gt;
&lt;h5&gt;更新(Updation)：&lt;/h5&gt;
&lt;p&gt;props发生变化后对应的更新过程：componentWillReceiveProps(父组件发生数据更改，父组件的render重新被执行，子组件预测到可能会发生替换新数据)、shouldComponentUpdate(询问是否应该更新？返回true则更新、返回flash则不更新)、componentWillUpate(准备要开始更新)、render(更新)、componentDidUpdate(更新完成)&lt;/p&gt;
&lt;p&gt;变量数据发生变化后对应的更新过程：shouldComponentUpdate(询问是否应该更新？返回true则更新、返回flash则不更新)、conponentWillUpdate(准备要开始更新)、、render(更新)、componentDidUpdate(更新完成)&lt;/p&gt;
&lt;p&gt;props和states发生变化后的更新过程，唯一差异是props多了一个 componentWillReceiveProps生命周期函数。&lt;/p&gt;
&lt;p&gt;componentWillReceiveProps触发的条件是：&lt;br /&gt;
1、一个组件要从父组件接收参数，并且已存在父组件中(子组件第一次被创建时是不会执行componentWillReceiveProps的)&lt;br /&gt;
2、只要父组件的render函数重新被执行(父组件发生数据更改，子组件预测到可能会发生替换新数据)，componentWillReceiveProps就会被触发&lt;/p&gt;
&lt;h5&gt;捕获子组件错误：&lt;/h5&gt;
&lt;p&gt;componentDidCatch(捕获到子组件错误时被触发)&lt;/p&gt;
&lt;h5&gt;卸载(Unmounting)：&lt;/h5&gt;
&lt;p&gt;componentWillUnmount(即将被卸载)&lt;/p&gt;
&lt;p&gt;备注：类组件继承自Component组件，Component组件内置了除render()以外的所有生命周期函数。因此自定义组件render()这个生命周期函数必须存在，其他的生命周期函数都可以忽略不写。 而使用了hook的函数组件，简化了生命周期函数调用的复杂程度。&lt;/p&gt;
&lt;h5&gt;生命周期函数的几个应用场景：&lt;/h5&gt;
&lt;p&gt;对于类组件(由class创建的)和函数组件(由function创建的)，他们对于生命周期的调用方法不同。&lt;/p&gt;
&lt;p&gt;1、只需要第一次获取数据的Ajax请求&lt;br /&gt;
如果类组件有ajax请求(只需请求一次)，那么最好把ajax请求写在componentDidMount中(只执行一次)。因为&quot;初始化、挂载、卸载&quot;在一个组件的整个生命周期中只会发生一次，而&quot;更新&quot;可以在生命周期中多次执行。&lt;br /&gt;
如果是函数组件，则可以写在useEffect()中，并且将第2个参数设置为空数组，这样useEffect只会执行一次。&lt;/p&gt;
&lt;p&gt;2、防止子组件不必要的重新渲染&lt;br /&gt;
如果是类组件，父组件发生变量改变，那么会调用render()，会重新渲染所有子组件。但是如果变量改变的某个值与某子组件并不相关，如果此时也重新渲染该子组件会造成性能上的浪费。为了解决这个情况，可以在子组件中的shouldComponentUpdate生命周期函数中，做以下操作:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    shouldComponentUpdate(nextProps,nextStates){
      //判断xxx值是否相同，如果相同则不进行重新渲染
      return (nextProps.xxx !== this.props.xxx); //注意是 !== 而不是 !=
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还可以让组件继承由React.Component改为React.PureComponent，react会自动帮我们在shouldComponentUpdate生命周期函数中做浅对比。&lt;/p&gt;
&lt;p&gt;如果是函数组件，则在子组件导出时，使用React.memo()进行包裹，同时结合useCallback来阻止无谓的渲染，实现提高性能。&lt;/p&gt;
&lt;h1&gt;React中设置样式的几种形式&lt;/h1&gt;
&lt;h5&gt;第一种：引用外部css样式&lt;/h5&gt;
&lt;p&gt;伪代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import from &apos;./xxx.css&apos;;  
return &amp;lt;div className=&apos;xxx&apos; /\&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：在jsx语法中，使用驼峰命名。例如原生html中的classname需要改成className、background-color要改成backgroundColor。&lt;/p&gt;
&lt;h5&gt;第二种：内部样式&lt;/h5&gt;
&lt;p&gt;伪代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return &amp;lt;div style={{backgroundColor:&apos;green&apos;,width:&apos;100px&apos;}} /\&amp;gt;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：内联样式值为一个对象，对象属性之间用&quot;,&quot;分割而不是原生html中的&quot;;&quot;。&lt;br /&gt;
因为是一个对象，因此下面代码也是可行的：&lt;br /&gt;
&lt;code&gt;const mystyle = {backgroundColor:&apos;green&apos;,width:&apos;100px&apos;};&lt;/code&gt;
&lt;code&gt;return &amp;lt;div style={mystyle} /\&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;h1&gt;Hook用法&lt;/h1&gt;
&lt;p&gt;Hook是react16.8以上版本才出现的新特性，可以在函数组件中使用组件生命周期函数，且颗粒度更加细致。&lt;/p&gt;
&lt;p&gt;可以把Hook逻辑从组件中抽离出来，多个组件可以共享该hook逻辑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;请注意hook本质上是为了解决组件之间共享逻辑，并不是单纯为了解决组件之间共享数据。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;hook表现出来特别像一个普通的JS函数(仅仅是表现出来但绝不是真的普通JS函数)。&lt;/p&gt;
</content:encoded></item><item><title>19 useTransition基础用法</title><link>https://blog.wemang.com/posts/web/react/usetransition%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/usetransition%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</guid><pubDate>Mon, 18 Sep 2023 16:45:22 GMT</pubDate><content:encoded>&lt;h2&gt;19 useTransition基础用法&lt;/h2&gt;
&lt;h2&gt;useTransition概念介绍&lt;/h2&gt;
&lt;p&gt;react提供了&lt;code&gt;useDeferredValue&lt;/code&gt;发挥类似防抖节流的作用，而&lt;code&gt;useTransition&lt;/code&gt;也是类似的作用，但是该hook是通过降低数据渲染的优先级来达到优先更新其他数据&lt;/p&gt;
&lt;h2&gt;useTransition用来解决什么问题？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;首先给定一个场景，开发时经常会遇到需要联想输入，也就是输入的同时要返回联想搜索结果的列表。&lt;/li&gt;
&lt;li&gt;但是这个列表有时返回值非常的长，有时会导致用户输入值的更新缓慢，这里就产生了一个问题，当页面有大量UI更新的时候，怎么处理数据更新不会卡顿。&lt;/li&gt;
&lt;li&gt;可以手写防抖节流，防抖有一个弊端，当我们长时间的持续输入（时间间隔小于防抖设置的时间），页面就会长时间都不到响应。而startTransition 可以指定 UI 的渲染优先级，哪些需要实时更新，哪些需要延迟更新。即使用户长时间输入最迟 5s 也会更新一次。&lt;/li&gt;
&lt;li&gt;也可以用&lt;code&gt;useDeferredValue&lt;/code&gt;，也可以用“可视窗口加载”的方案，
在这里介绍怎么用&lt;code&gt;useTransition&lt;/code&gt;解决&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;useTransition源码&lt;/h2&gt;
&lt;p&gt;回到useTransition的学习中，首先看一下React源码中的&lt;a href=&quot;https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js&quot;&gt;ReactHooks.js&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function useTransition(): [
  boolean,
  (callback: () =&amp;gt; void, options?: StartTransitionOptions) =&amp;gt; void,
] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useTransition();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再根据引入文件，到&lt;code&gt;react-reconciler/src/ReactInternalTypes.js&lt;/code&gt;找到Dispatch里最终调用的useTransition&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  useTransition(): [
    boolean,
    (callback: () =&amp;gt; void, options?: StartTransitionOptions) =&amp;gt; void,
  ],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码看不懂没关系，本系列教程只是讲述“如何使用Hook”，并不是“Hook源码分析”。^_^&lt;/p&gt;
&lt;h2&gt;useTransition基本用法&lt;/h2&gt;
&lt;p&gt;useTransition()函数可以不传参，传参可以传一个毫秒值用来修改最迟更新时间，startTransition回调里的赋值将会被降低优先级。isPending 指示过渡任务何时活跃以显示一个等待状态。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;代码形式&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const [isPending, startTransition] = useTransition();

startTransition(() =&amp;gt; {
  setCount(count + 1);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;传参写法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 延迟两秒
const [isPending, startTransition] = useTransition(2000);

startTransition(() =&amp;gt; {
  setCount(count + 1);
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;useTransition使用示例&lt;/h2&gt;
&lt;p&gt;举例：搜索引擎的关键词联想。一般来说，对于用户在输入框中输入都希望是实时更新的，如果此时联想词比较多同时也要实时更新的话，这就可能会导致用户的输入会卡顿。这样一来用户的体验会变差，这并不是我们想要的结果。&lt;/p&gt;
&lt;p&gt;我们将这个场景的状态更新提取出来：一个是用户输入的更新；一个是联想词的更新，这个两个更新紧急程度显然前者大于后者。&lt;/p&gt;
&lt;p&gt;这里更新效果可能还不够明显，可以打开浏览器控制台，点击&lt;code&gt;performance insights&lt;/code&gt;项，在Measure page load右边有个下拉选项，在cpu那栏的右边下拉选择&lt;code&gt;4x slowdown&lt;/code&gt;可以将浏览器运行速度调慢四倍，这样卡顿会明显些。&lt;/p&gt;
&lt;p&gt;这里拆分为两个组件，父组件是useTransition的使用，子组件是列表渲染，代码如下：&lt;/p&gt;
&lt;p&gt;父组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useState, useTransition } from &apos;react&apos;
import ProductList from &apos;./components/ProductList&apos;

// 列表数据的生成
export function generateProducts() {
  const products: Array&amp;lt;string&amp;gt; = []
  for (let i = 0; i &amp;lt; 10000; i++) {
    products.push(`Product ${i + 1}`)
  }
  return products
}

// 列表数据
const dummyProducts = generateProducts()

// 用户输入时过滤搜索，达到一个联想输入的效果
function filterProducts(filterTerm) {
  if (!filterTerm) {
    return dummyProducts
  }
  return dummyProducts.filter((product) =&amp;gt; product.includes(filterTerm))
}

function App() {
  const [isPending, startTransition] = useTransition()
  const [filterTerm, setFilterTerm] = useState(&apos;&apos;)

  const filteredProducts = filterProducts(filterTerm)

  function updateFilterHandler(event) {
    // 列表数据赋值的运行等级
    startTransition(() =&amp;gt; {
      setFilterTerm(event.target.value)
    })
  }

  return (
    &amp;lt;div id=&quot;app&quot;&amp;gt;
      &amp;lt;input type=&quot;text&quot; onChange={updateFilterHandler} /&amp;gt;
      {isPending &amp;amp;&amp;amp; &amp;lt;p style={{ color: &apos;white&apos; }}&amp;gt;更新列表。. &amp;lt;/p&amp;gt;}
      &amp;lt;ProductList products={filteredProducts} /&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}

export default App
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;子组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useDeferredValue } from &quot;react&quot;;

function ProductList({ products }) {
  const deferredProducts = useDeferredValue(products);
  return (
    &amp;lt;ul&amp;gt;
      {deferredProducts.map((product, index) =&amp;gt; (
        &amp;lt;li key={index}&amp;gt;{product}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}

export default ProductList;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这个案例，相信你对useMemo的机制和用法一定有所掌握。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useTransition基础用法已经讲完。&lt;/p&gt;
</content:encoded></item><item><title>18 示例：React使用Echarts所用到的hooks</title><link>https://blog.wemang.com/posts/web/react/%E7%A4%BA%E4%BE%8Breact%E4%BD%BF%E7%94%A8echarts%E6%89%80%E7%94%A8%E5%88%B0%E7%9A%84hooks/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/%E7%A4%BA%E4%BE%8Breact%E4%BD%BF%E7%94%A8echarts%E6%89%80%E7%94%A8%E5%88%B0%E7%9A%84hooks/</guid><pubDate>Mon, 18 Sep 2023 16:43:57 GMT</pubDate><content:encoded>&lt;h1&gt;18 示例：React使用Echarts所用到的hooks&lt;/h1&gt;
&lt;p&gt;本篇文章写于 2020年11月13日，距离前面文章已经过去半年，因此本文的讲述风格和示例代码，可能和前面的章节不同。&lt;/p&gt;
&lt;h2&gt;Echarts简介&lt;/h2&gt;
&lt;p&gt;Echarts 是百度公司推出的，基于原生 JS 的图表库，免费开源 ，可用于数据可视化项目。&lt;/p&gt;
&lt;p&gt;官网地址：https://echarts.apache.org/zh/feature.html&lt;/p&gt;
&lt;h2&gt;Echarts基础操作&lt;/h2&gt;
&lt;p&gt;1、&lt;strong&gt;Echarts 是基于原生 JS 的库，而不是 React 组件&lt;/strong&gt;，需要将 “图表” 挂载到 DOM&lt;/p&gt;
&lt;p&gt;2、echarts.init(xxx-dom) 是创建 “图表” 的入口函数，该函数将创建创建真正的图表实例，并填充到 xxx-dom 中&lt;/p&gt;
&lt;p&gt;3、一个图表 对应一个 DOM，N 个图标需要 N 个 DOM&lt;/p&gt;
&lt;p&gt;4、图表实例通过 setOption(option) 来设置(更新)数据&lt;/p&gt;
&lt;h2&gt;针对以上Echarts特性，对应的 hooks&lt;/h2&gt;
&lt;p&gt;1、使用 useRef 来勾住 jsx 中的某个 DOM&lt;/p&gt;
&lt;p&gt;2、使用 useEffect( () =&amp;gt; {}, [] ) 来勾住 React 第一次挂载，并通过 echarts.init(xxx-dom) 创建出真正的图表&lt;/p&gt;
&lt;p&gt;3、使用 useState 来勾住 创建出的真正图表，以便以后做各种更新操作&lt;/p&gt;
&lt;p&gt;4、使用 useEffect( () =&amp;gt; {}, [xxx-echart,option] ) 来不断监听组件传递过来的数据变化，并更新图表数据&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;补充说明：尽管 NPM 中已经有 echarts-for-react 这个包，已经将 Echarts 封装成可直接使用的 React 组件，但是我并不是建议使用，因为毕竟 Echarts 并不是特别难，没有必要使用别人封装好的。学习本文后，你自己也可以轻松封装自己的 Echarts 组件，灵活方便。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用(封装)Echarts示例代码&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;细节不过多说，此处只演示 2 个组件源码，使用 TypeScript 编写&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;子组件为一个图表，图表是什么类型，由 配置数据 option 中 xAxis.type 的值决定&lt;/li&gt;
&lt;li&gt;父组件负责调用子组件并传递图表配置数据&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;父组件：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &apos;react&apos;
import { EChartOption } from &apos;echarts&apos;
import Echart from &apos;../../components/echart&apos;

import &apos;./index.scss&apos;

const option: EChartOption = {
    xAxis: {
        type: &apos;category&apos;,
        data: [&apos;Mon&apos;, &apos;Tue&apos;, &apos;Wed&apos;, &apos;Thu&apos;, &apos;Fri&apos;, &apos;Sat&apos;, &apos;Sun&apos;]
    },
    yAxis: {
        type: &apos;value&apos;
    },
    series: [{
        data: [820, 932, 901, 934, 1290, 1330, 1320],
        type: &apos;line&apos;
    }]
}

const IndexPage: React.FC = () =&amp;gt; {
    return (
        &amp;lt;Echart option={option} /&amp;gt;
    )
}

export default IndexPage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;子组件：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { useState, useRef, useEffect } from &apos;react&apos;
import echarts, { EChartOption, ECharts } from &apos;echarts&apos;

import &apos;./index.scss&apos;

interface EchartProp {
    option: EChartOption
}

const Echart: React.FC&amp;lt;EchartProp&amp;gt; = ({ option }) =&amp;gt; {

    const chartRef = useRef&amp;lt;HTMLDivElement&amp;gt;(null) //用来勾住渲染后的 DOM
    const [echartsInstance, setEchartsInstance] = useState&amp;lt;ECharts&amp;gt;() //用来勾住生成后的 图表实例对象

    //仅第一次挂载时执行，将 DOM 传递给 echarts，通过 echarts.init() 得到真正的图表 JS 对象
    useEffect(() =&amp;gt; {
        if (chartRef.current) {
            setEchartsInstance(echarts.init(chartRef.current))
        }
    }, [])

    //监听依赖变化，并根据需要更新图表数据
    useEffect(() =&amp;gt; {
        echartsInstance?.setOption(option)
    }, [echartsInstance, option])

    return (
        &amp;lt;div ref={chartRef} className=&apos;echarts&apos; /&amp;gt;
    )
}

export default Echart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上示例中，父组件功能相对简单，负责调用子组件，并将图表配置数据传递给子组件。&lt;/p&gt;
&lt;p&gt;真正需要关注的就是 子组件，在子组件中分别用到了 useRef、useState、useEffect 这 3 个 hook，尤其是 useEffect 还被使用了 2 次。&lt;/p&gt;
</content:encoded></item><item><title>17 React Hook 总结</title><link>https://blog.wemang.com/posts/web/react/react-hook-%E6%80%BB%E7%BB%93/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/react-hook-%E6%80%BB%E7%BB%93/</guid><pubDate>Mon, 18 Sep 2023 16:40:25 GMT</pubDate><content:encoded>&lt;h1&gt;17 React Hook 总结&lt;/h1&gt;
&lt;p&gt;首先，对你能够坚持到现在，表示深深的赞扬，学习React Hook之路不容易。&lt;/p&gt;
&lt;p&gt;我们快速的回顾一下之前学习过的各个hook。&lt;/p&gt;
&lt;h2&gt;react hook 回顾&lt;/h2&gt;
&lt;h5&gt;定义变量&lt;/h5&gt;
&lt;p&gt;useState()：定义普通变量&lt;br /&gt;
useReducer()：定义有不同类型、参数的变量&lt;/p&gt;
&lt;h5&gt;组件传值&lt;/h5&gt;
&lt;p&gt;useContext()：定义和接收具有全局性质的属性传值对象，必须配合React.createContext()使用&lt;/p&gt;
&lt;h5&gt;对象引用&lt;/h5&gt;
&lt;p&gt;useRef()：获取渲染后的DOM元素对象，可调用该对象原生html的方法，可能需要配合React.forwardRef()使用&lt;br /&gt;
useImperativeHandle()：获取和调用渲染后的DOM元素对象拥有的自定义方法，必须配合React.forwardRef()使用&lt;/p&gt;
&lt;h5&gt;生命周期&lt;/h5&gt;
&lt;p&gt;useEffect()：挂载或渲染完成后、即将被卸载前，调度&lt;br /&gt;
useLayoutEffect()：挂载或渲染完成后，同步调度&lt;/p&gt;
&lt;h5&gt;性能优化&lt;/h5&gt;
&lt;p&gt;useCallback()：获取某处理函数的引用，必须配合React.memo()使用&lt;br /&gt;
useMemo()：获取某处理函数返回值的副本&lt;/p&gt;
&lt;h5&gt;代码调试&lt;/h5&gt;
&lt;p&gt;useDebugValue()：对react开发调试工具中的自定义hook，增加额外显示信息&lt;/p&gt;
&lt;h5&gt;自定义hook&lt;/h5&gt;
&lt;p&gt;useCustomHook()：将hook相关逻辑代码从组件中抽离，提高hook代码可复用性&lt;/p&gt;
&lt;h2&gt;react hook 扩展阅读&lt;/h2&gt;
&lt;p&gt;附01：React基础知识
附02：React扩展阅读&lt;/p&gt;
&lt;h2&gt;信息反馈&lt;/h2&gt;
&lt;p&gt;若有错误欢迎指正，本人QQ (1933668022)，或通过邮件联系：allen@183.co&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，React Hook 你已学习完成。&lt;/p&gt;
&lt;p&gt;真心为你鼓掌，加油，在实践中去提升自己的 React Hook 战斗值吧。&lt;/p&gt;
</content:encoded></item><item><title>16 自定义hook</title><link>https://blog.wemang.com/posts/web/react/%E8%87%AA%E5%AE%9A%E4%B9%89hook/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/%E8%87%AA%E5%AE%9A%E4%B9%89hook/</guid><pubDate>Mon, 18 Sep 2023 16:40:07 GMT</pubDate><content:encoded>&lt;h1&gt;16 自定义hook&lt;/h1&gt;
&lt;h2&gt;自定义hook概念解释&lt;/h2&gt;
&lt;p&gt;像useState、useEffect、useContext、useReducer、useCallback、useMemo、useRef、useImperativeHandle、useLayoutEffect、useDebugValue这10个hook是react默认自带的hook，而所谓自定义hook就是由我们自己编写的hook。&lt;/p&gt;
&lt;p&gt;所谓自定义hook就是把原来写在函数组件内的hook相关代码抽离出来，单独定义成一个函数，而这个抽离出来的hook函数就称之为“自定义hook钩子函数”，简称“自定义hook”。&lt;/p&gt;
&lt;h2&gt;自定义hook是来解决什么问题的？&lt;/h2&gt;
&lt;p&gt;答：自定义hook是将原来在组件中编写的相关hook代码抽离出组件，让hook相关代码独立存在，达到优化代码结构、相关hook代码可以重复使用的目的。&lt;/p&gt;
&lt;p&gt;补充说明：&lt;br /&gt;
1、如果你在别人的项目代码中，发现除了react默认自带的那10个hook以外，出现了 useXxx() 这样的看着像hook的函数，可以肯定那些就是自定义的hook。&lt;br /&gt;
2、随着react新版本发布，可能会出现更多新的、默认自带的hook。&lt;/p&gt;
&lt;h2&gt;自定义hook基本用法&lt;/h2&gt;
&lt;p&gt;首先我们知道hook只能用在函数组件中，而函数组件本身是一个稍微特殊的函数，尽管稍微特殊但毕竟他也遵循一般函数的使用规律。 所谓“把原来写在函数组件内的hook相关代码抽离出来，单独定义成一个函数” 本质上就是把函数内部定义的变量或方法拿出来，放到函数外面单独定义成一个函数。&lt;/p&gt;
&lt;p&gt;这个抽离出来新定义的函数，遵循JS默认的函数用法，即函数参数可以任意设定，返回值也可以是任意内容。&lt;/p&gt;
&lt;p&gt;请注意：react规定所有的自定义hook函数命名时必须使用 useXxx 这种形式。&lt;/p&gt;
&lt;p&gt;举一个最简单的例子：假设我们有一个组件，组件内部有一个count的变量，我们的代码之前是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{useState} from &apos;react&apos;
    function CurrentComponent() {
      const [count,setCount] = useState(0);//请注意这行代码，就是我们要即将抽离出去的hook
      return (
        &amp;lt;div&amp;gt;
            {count}
            &amp;lt;button onClick={() =&amp;gt; setCount(count+1)}&amp;gt;add +1&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }
    export default CurrentComponent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面这个组件中，通过 &lt;code&gt;const [count,setCount] = useState(0)&lt;/code&gt; 定义了组件内的变量count和修改count的方法。那我们现在将这行相关的hook抽离出函数组件。我们计划把抽离出来的、和count相关的函数，命名为useCount，修改后的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //useCount.js
    import {useState} from &apos;react&apos;；
    function useCount(initialValue){

      //依然使用 useState 创建countcount和setCount
      //并且将参数initialValue的值赋予给count作为默认值
      //将创建好的count和setCount作为函数返回值 return 出去

      const [count,setCount] = useState(initialValue);
      return [count,setCount];
    }
    export default useCount;

    //CurrentComponent.js
    import React from &apos;react&apos;
    import useCount from &apos;./useCount&apos;;//引入useCount
    function CurrentComponent() {
      const [count,setCount] = useCount(0);//请注意这里使用的是useCount，而不是useState
      return (
        &amp;lt;div&amp;gt;
            {count}
            &amp;lt;button onClick={() =&amp;gt; setCount(count+1)}&amp;gt;add +1&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }
    export default CurrentComponent

&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;代码分析：&lt;/h5&gt;
&lt;p&gt;1、我们将原本组件内定义的count相关代码抽离出组件，单独定义一个 useCount 函数。&lt;br /&gt;
2、组件需要使用 useCount，只需先引入useCount，然后把 useCount 当成普通函数使用就好了。&lt;br /&gt;
3、useCount就是我们自定义的hook。&lt;/p&gt;
&lt;p&gt;注意：一般自定义hook顶部是不需要引入React的，只需要引入使用到的 hook 函数即可。&lt;br /&gt;
例如上面 useCount 顶部，我们写的是 &lt;code&gt;import {useState} from &apos;react&apos;&lt;/code&gt; 而不是 &lt;code&gt;import React,{useState} from &apos;react&apos;&lt;/code&gt;；&lt;/p&gt;
&lt;p&gt;上面举例中的useCount非常简单，内部并没有过多逻辑，在实际开发中自定义hook内部肯定要有比较复杂的逻辑。&lt;/p&gt;
&lt;p&gt;由于是单独定义的，所以自定义hook可以同时被多个组件引入和使用，达到代码复用的目的。&lt;/p&gt;
&lt;p&gt;划重点，在实际项目中，通常自定义hook返回值有3种表现形式：&lt;br /&gt;
1、不带返回值的函数&lt;br /&gt;
2、带普通返回值的函数&lt;br /&gt;
3、带特殊结构返回值的函数&lt;/p&gt;
&lt;p&gt;以上3种不同返回值各有各的适用场景，下面就以实际示例来逐一说明。&lt;/p&gt;
&lt;h2&gt;不带返回值的自定义hook使用示例：&lt;/h2&gt;
&lt;p&gt;举例：若父组件内有多个子组件，每个子组件内部都有不同的业务代码，但是所有子组件有一个相同的功能，就是当自身内部变量value发生变化时，将网页标题改为变量value的值。&lt;/p&gt;
&lt;p&gt;首先我们知道修改网页标题是在组件内部的useEffect()函数中修改，那结合上面的使用场景，我们可以将useEffect()单独抽离出来，作为一个自定义hook，命名为 useDocumentTitle，让所有子组件都复用这个useDocumentTitle。&lt;/p&gt;
&lt;p&gt;useDocumentTitle 代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import {useEffect} from &apos;react&apos;
    function useDocumentTitle(value) {
      useEffect(() =&amp;gt; {
        document.title = value;
      },[value]);
    }
    export default useDocumentTitle;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设我们其中一个子组件的功能为：&lt;br /&gt;
1、有1个number类型的变量count&lt;br /&gt;
2、有1个按钮，点击按钮后将count修改为一个随机数字&lt;br /&gt;
3、当组件重新渲染完成后，将网页标题修改为count的值&lt;/p&gt;
&lt;p&gt;子组件代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{useState} from &apos;react&apos;
    import useDocumentTitle from &apos;./useDocumentTitle&apos;;
    function ChildComponent() {
      const [count,setCount] = useState(0);
      useDocumentTitle(count);//把内部变量count传给useDocumentTitle，既作为网页标题内容，同时也作为useEffect的变量依赖
      return (
        &amp;lt;div&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; setCount(Math.floor(Math.random()*1000))}&amp;gt;click me&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }
    export default ChildComponent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他子组件也使用useDocumentTitle，这样我们便将原本每个子组件都需要编写的useEffect改为统一的useDocumentTitle，实现了代码复用。&lt;/p&gt;
&lt;p&gt;应用场景小总结：&lt;br /&gt;
在这个示例中，useDocumentTitle函数并没有任何返回值，子组件使用useDocumentTitle时就好像原本那段useEffect代码本身就定义在那里似的。&lt;/p&gt;
&lt;h2&gt;带普通返回值的自定义hook使用示例：&lt;/h2&gt;
&lt;p&gt;在本章最开始讲解“自定义hook基本用法”时，所举的useCount例子非常简单，这次我们将对useCount进行功能上的扩展。&lt;/p&gt;
&lt;p&gt;原本useCount只是定义了count和setCount，这次所谓的功能扩展，就是将setCount改为其他几种修改count的函数。&lt;br /&gt;
例如：&lt;br /&gt;
1、添加 add()&lt;br /&gt;
2、减去 sub()&lt;br /&gt;
3、相乘 mul()&lt;br /&gt;
4、恢复初始值 reset()&lt;/p&gt;
&lt;p&gt;修改后的useCount代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import {useState} from &apos;react&apos;
    function useCount(initialValue){
      const [count,setCount] = useState(initialValue);
      const add = param =&amp;gt; {setCount(prev =&amp;gt; prev + param);}
      const sub = param =&amp;gt; {setCount(prev =&amp;gt; prev - param);}
      const mul = param =&amp;gt; {setCount(prev =&amp;gt; prev * param);}
      const reset = () =&amp;gt; {setCount(() =&amp;gt; initialValue);}
      return [count,add,sub,mul,reset]; //将count和定义的4个方法作为返回值 return 出去
    }
    export default useCount;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请注意：为了避免4个修改函数中得到的是旧的count，所以我们采用的是 &lt;code&gt;setCount(prev =&amp;gt; xxxxx)&lt;/code&gt; 这种修改方式，而不是直接使用 &lt;code&gt;setCount(count xxx)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;CurrentComponent组件想使用useCount，代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React from &apos;react&apos;
    import useCount from &apos;./useCount&apos;

    function CurrentComponent() {
      const [count,add,sub,mul,reset] = useCount(0); //使用useCount，并解构useCount的返回值
      return (
        &amp;lt;div&amp;gt;
            {count}
            &amp;lt;button onClick={() =&amp;gt; {add(1)}}&amp;gt;+ 1&amp;lt;/button&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; {sub(1)}}&amp;gt;- 1&amp;lt;/button&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; {mul(2)}}&amp;gt;* 2&amp;lt;/button&amp;gt;
            &amp;lt;button onClick={() =&amp;gt; {reset()}}&amp;gt;reset&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }
    export default CurrentComponent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于上面这个效果，你是否觉得眼熟？  没错，在讲解useReducer时，就使用useReducer实现了类似的一个效果。  只不过这次是为了讲解自定义hook，而那次是讲解如何使用useReducer替代useState实现复杂的业务。&lt;/p&gt;
&lt;p&gt;应用场景小总结：&lt;br /&gt;
1、我们可以在自定义hook中编写相关业务逻辑函数(方法)，并通过返回值的形式 return 出去，供其他组件调用。&lt;/p&gt;
&lt;h2&gt;带特殊结构返回值的自定义hook使用示例：&lt;/h2&gt;
&lt;p&gt;上一个代码示例讲解了“带普通返回值”的自定义hook，那这次要讲的“带特殊结构返回值”的自定义hook究竟差别在哪里？&lt;/p&gt;
&lt;p&gt;“带特殊结构返回值”中的特殊是指？&lt;br /&gt;
答：我们把组件需要用到的多项属性设置，合并为一个对象 并 return 出去，供组件使用。&lt;/p&gt;
&lt;p&gt;还是以示例来讲解会更容易理解，假设我们有一个登录组件，功能为：&lt;br /&gt;
1、有一个用户名输入框&lt;br /&gt;
2、有一个密码输入框&lt;br /&gt;
3、有一个提交按钮&lt;/p&gt;
&lt;p&gt;补充说明，为了简化代码，我们并不做真正的登录验证，点击提交按钮后：&lt;br /&gt;
1、仅仅是alert一下用户名和密码，即表示登录&lt;br /&gt;
2、同时清除用户名和密码输入框里的内容&lt;/p&gt;
&lt;p&gt;需求分析：&lt;br /&gt;
1、每个输入框都是一个&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;，都需要绑定一个变量，都需要设置onChange事件&lt;br /&gt;
2、每一个输入框都需要清空内容&lt;/p&gt;
&lt;p&gt;我们将定义一个自定义hook，命名为useInput，useInput来实现这2个输入框共有的业务逻辑。&lt;/p&gt;
&lt;p&gt;useInput的代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import {useState} from &apos;react&apos;
    function useInput(initialValue) {
      const [value,setValue] = useState(initialValue); //定义输入框对应的值value
      //定义reset函数，用来重置输入框
      const reset = () =&amp;gt; {
        setValue(initialValue);
      }
      //定义一个 bind 对象，该对象有 value 和 onChange 2个属性
      const bind = {
        value,
        onChange: eve =&amp;gt; {
            setValue(eve.target.value)
        }
      }
      return [value,reset,bind];//将输入框的值、重置输入框函数、定义的bind对象作为返回值 return 出去
    }
    export default useInput
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请注意：在useInput中，返回值 value、reset 我们很容易理解，但是 bind 是来做什么的？&lt;br /&gt;
答：这个 bind 就是我们前面提到的“带特殊结构返回值”，bind对象本身结构由2个属性value和onChange组成。&lt;br /&gt;
至于 bind 怎么用，很快揭晓。&lt;/p&gt;
&lt;p&gt;登录组件LoginForm的代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React from &apos;react&apos;
    import useInput from &apos;./useInput&apos;;
    function LoginForm() {
      const [usename,resetUsename,bindUsename] = useInput(&apos;&apos;); //定义用户名输入框相关的变量
      const [password,resetPassword,bindPassword] = useInput(&apos;&apos;); //定义密码输入框相关的变量

      const submitHandle = (eve) =&amp;gt; {
        eve.preventDefault(); //阻止form真正提交
        alert(`usename:${usename}\rpassword:${password}`); //通过alert，弹出用户名和密码的值
        resetUsename(); //重置用户名输入框
        resetPassword(); //重置密码输入框
      }

      //请特别留意用户名和密码输入框中的 {...bindUsename}和{...bindPassword}
      return (
        &amp;lt;form onSubmit={submitHandle}&amp;gt;
            &amp;lt;label&amp;gt;usename:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&apos;text&apos; {...bindUsename} /&amp;gt;
            &amp;lt;label&amp;gt;password:&amp;lt;/label&amp;gt;
            &amp;lt;input type=&apos;password&apos; {...bindPassword} /&amp;gt;
            &amp;lt;input type=&apos;submit&apos; value=&apos;login&apos; /&amp;gt;
        &amp;lt;/form&amp;gt;
      )
    }
    export default LoginForm;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于获取输入框的值、以及调用输入框对应的reset()函数，相信你很容易理解。&lt;/p&gt;
&lt;p&gt;下面对 &lt;code&gt;{...bindUsename}&lt;/code&gt; 和 &lt;code&gt;{...bindPassword}&lt;/code&gt; 做进一步说明：&lt;br /&gt;
1、首先我们知道 &lt;code&gt;{...obj}&lt;/code&gt; 这种在原生JS中，相当于把obj对象进行解构，然后得到一个浅拷贝的新对象。&lt;br /&gt;
2、但是在上面的代码中并不是这个意思，千万不要被迷惑。 在JSX中的某组件，如果要添加某属性，格式为 &lt;code&gt;xxx={xxx}&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;例如常见的给一个输入框绑定某变量，同时添加onChange事件，一般写法为：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;input type=&apos;text&apos; value={xx} onChange={xxxx} /&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而我们本次代码中，采用的是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;input type=&apos;text&apos; {...bindUsename} /&amp;gt;
    &amp;lt;input type=&apos;password&apos; {...bindPassword} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里面的 &lt;code&gt;{...bindUsename}&lt;/code&gt;  &lt;code&gt;{...bindPassword}&lt;/code&gt; 其实相当于把 bindUsename 和 bindPassword 进行了解构，就好像直接写在这里似的。&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;中有非常多相同的属性，那么把这些相同属性提炼到 useInput 的 bind 中，这样可以简化组件里的代码。&lt;/p&gt;
&lt;p&gt;应用场景小总结：&lt;br /&gt;
1、在自定义hook中，将组件需要的多项属性合并成一个对象，供组件属性解构使用，会简化组件代码，提高代码复用率。&lt;/p&gt;
&lt;p&gt;相信通过上面3个示例，对自定义hook的返回值不同形式的演示，举一反三，会帮助你灵活的编写自定义hook。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于自定义hook已经讲完。&lt;/p&gt;
&lt;p&gt;我们对之前所有学过的hook进行一次小总结。&lt;/p&gt;
</content:encoded></item><item><title>15 useDebugValue基础用法</title><link>https://blog.wemang.com/posts/web/react/usedebugvalue%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/usedebugvalue%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</guid><pubDate>Mon, 18 Sep 2023 16:01:07 GMT</pubDate><content:encoded>&lt;h1&gt;15 useDebugValue基础用法&lt;/h1&gt;
&lt;h2&gt;useDebugValue概念解释&lt;/h2&gt;
&lt;p&gt;我们第十个要学习的Hook(钩子函数)是useDebugValue，他的作用是“勾住”React开发调试工具中的自定义hook标签，让useDebugValue勾住的自定义hook可以显示额外的信息。&lt;/p&gt;
&lt;h5&gt;“React开发调试工具”是什么？&lt;/h5&gt;
&lt;p&gt;答：谷歌浏览器中的一个扩展插件，名字叫“React Developer Tools”，方便我们在谷歌浏览器上进行react项目调试。&lt;/p&gt;
&lt;p&gt;如何安装？&lt;br /&gt;
答：可在Chrome扩展程序商店搜索并安装。由于国内网络原因，如果你不会科学上网，那么可以通过国内的一些Chrome扩展程序商店网站，下载“React Developer Tools”离线的crx安装文件进行安装。具体办法可以自己百度。&lt;/p&gt;
&lt;p&gt;“React开发调试工具”的使用简单说明：&lt;br /&gt;
如果该扩展程序安装成功，那么会有以下几种情况：&lt;br /&gt;
1、对于本机开发调试的项目网页，该插件图标会变成橘黄色，且图标中间有一个小虫子，表示可以进行react源码式的调试，当代码出现错误时会精准定位出错的代码位置。&lt;/p&gt;
&lt;p&gt;2、对于别人开发的项目网页，该插件图标会变成蓝色，表示该网页由react开发，当代码出现错误时不能精准定位出错的代码位置。&lt;br /&gt;
例如阿里云后台、腾讯云后台、百度翻译这些网页都是用react开发，访问这些网页你就会看到 调试工具图光标为蓝色。  这些大厂都用react，所以虽然学习过程中很痛苦，但是是值得的。&lt;/p&gt;
&lt;p&gt;3、对于没有使用react的网页，该插件图标会变成灰色。&lt;/p&gt;
&lt;p&gt;让我们回到useDebugValue基础学习中。&lt;/p&gt;
&lt;h2&gt;useDebugValue是来解决什么问题的？&lt;/h2&gt;
&lt;p&gt;答：useDebugValue的目的是“在react开发者工具自定义hook标签中显示额外信息”，方便我们“一眼就能找到”对应的自定义hook。&lt;/p&gt;
&lt;p&gt;补充说明：&lt;br /&gt;
1、react官网文档中明确表示，在普通项目开发中不推荐使用useDebugValue，默认的调试输出已经很清晰可用了。&lt;br /&gt;
2、除非你的自定义 hook 是作为共享库中的一部分才有价值。这样其他人更容易注意到你自定义的hook状态变化。&lt;/p&gt;
&lt;h5&gt;自定义hook？&lt;/h5&gt;
&lt;p&gt;你可能注意到本章中提到了“自定义hook”，没错。像之前学习的useState、useContext等等都是react自带的hook，这些默认的hook是我们项目开发所需要用到的各种钩子函数。&lt;/p&gt;
&lt;p&gt;但是实际开发中，我们需要借助这些基础的、默认的、自带的hook函数，通过组合以及添加业务逻辑代码，形成自己的hook函数。&lt;/p&gt;
&lt;p&gt;具体如何自定义hook，稍后会单独有一章如何“自定义hook”中详细讲述。&lt;/p&gt;
&lt;h2&gt;useDebugValue函数源码：&lt;/h2&gt;
&lt;p&gt;回到useDebugValue的学习中，首先看一下React源码中的&lt;a href=&quot;https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js&quot;&gt;ReactHooks.js&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //备注：源码采用TypeScript编写，如果不懂TS代码，阅读起来稍显困难
    export function useDebugValue&amp;lt;T&amp;gt;(
      value: T,
      formatterFn: ?(value: T) =&amp;gt; mixed,
    ): void {
        if (__DEV__) {
        const dispatcher = resolveDispatcher();
        return dispatcher.useDebugValue(value, formatterFn);
      }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码看不懂没关系，本系列教程只是讲述“如何使用Hook”，并不是“Hook源码分析”。^_^&lt;/p&gt;
&lt;h2&gt;useDebugValue基本用法&lt;/h2&gt;
&lt;p&gt;useDebugValue(value,formatterFn)函数第1个参数为我们要额外显示的内容变量。第2个参数是可选的，是对第1个参数值的数据化格式函数。&lt;/p&gt;
&lt;p&gt;请注意：&lt;br /&gt;
1、useDebugValue应该在自定义hook中使用，如果直接在组件内使用是无效的，不会报错也不会有任何额外信息展示。&lt;br /&gt;
1、一般调试不需要使用useDebugValue，除非你编写的hook是公共库中的一部分，实在是想凸显额外信息，引起别人注意。&lt;br /&gt;
2、如果使用useDebugValue，最好设置第2个参数，向react开发调试工具讲清楚如何格式化展示第1个参数。&lt;/p&gt;
&lt;h5&gt;代码形式：&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;useDebugValue(xxx, xxx =&amp;gt; xxxxx)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;拆解说明：&lt;/h5&gt;
&lt;p&gt;1、xxx 为我们要重点关注的变量。&lt;br /&gt;
2、&lt;code&gt;xxx =&amp;gt; xxxxx&lt;/code&gt; 是 &lt;code&gt;(xxx) =&amp;gt; {return xxxxx}&lt;/code&gt; 的简写。表明如何格式化变量xxx。&lt;/p&gt;
&lt;h2&gt;如何在react调试工具中查看useDebugValue表现形式&lt;/h2&gt;
&lt;p&gt;前提条件：&lt;br /&gt;
1、在谷歌浏览器中成功安装了react开发调试工具&lt;br /&gt;
2、react项目中使用了自定义hook，且hook中使用了useDebugValue&lt;/p&gt;
&lt;p&gt;那么你可以进行一下步骤：&lt;br /&gt;
1、打开react调试网页，例如http://localhost:3000/&lt;br /&gt;
2、打开谷歌浏览器调试面板(快捷键为F12)&lt;br /&gt;
3、找到并点击“Components”一栏&lt;br /&gt;
4、在右侧窗口中，找到“hooks”，在“hooks”下就能看到自定义hook中useDebugValue自定义显示的信息。&lt;/p&gt;
&lt;p&gt;具体还是以下面实际例子来说明。&lt;/p&gt;
&lt;h2&gt;useDebugValue使用示例：&lt;/h2&gt;
&lt;p&gt;举例：useTime是我们自定义的一个hook函数，那么在这个自定义hook中，可以通过useDebugValue对变量time进行额外信息展示。&lt;/p&gt;
&lt;p&gt;代码示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //自定义hook：useTime
    function useTime(date){
      const [time,setTime] = useState(date);
      useDebugValue(time,time =&amp;gt; new Date(time));//请注意这一行代码
      return [time,setTime];
    }

    //组件中使用useTime，伪代码片段
    const [time,setTime] = useTime(Date.now());//请注意此处使用的是自定义hook：useTime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码分析：&lt;br /&gt;
1、我们在自定义hook中，使用了useDebugValue&lt;br /&gt;
2、useDebugValue第1个参数是time，向react开发调试工具表明要重点关注的变量是time。&lt;br /&gt;
3、第2个参数是对time的一个格式化函数。由于time实际为一个时间戳数字，通过&lt;code&gt;time =&amp;gt; new Date(time)&lt;/code&gt;将时间戳转化成具体的可读时间字符串，例如此时此刻：Mon May 11 2020 14:27:39 GMT+0800 (中国标准时间)&lt;/p&gt;
&lt;h5&gt;具体表现&lt;/h5&gt;
&lt;p&gt;在谷歌浏览器调试面板的“Component”右侧，你会看到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hooks  
  time:Mon May 11 2020 14:27:39 GMT+0800 (中国标准时间)  
    State:1589178459090  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设不使用useDebuValue，默认看到的是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hooks  
  time: 
    State:1589178459090  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;“Mon May 11 2020 14:27:39 GMT+0800 (中国标准时间)  ”就是useDebugValue额外展示出的信息。&lt;br /&gt;
你甚至还可以使用模板字符串，对格式化数据进行修改，比如将原本的第2个参数 &lt;code&gt;time =&amp;gt; new Date(time)&lt;/code&gt; 修改为：&lt;code&gt;time =&amp;gt;${new Date(time)}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在谷歌浏览器调试面板的“Component”右侧，你会看到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hooks  
  time:看这里 Mon May 11 2020 14:27:39 GMT+0800 (中国标准时间)  
    State:1589178459090  
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;再次强调，对于一般性的项目开发，是不需要使用useDebugValue来额外标记某些变量的，默认的调试输出足够我们使用了。&lt;/h5&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useDebugValue基础用法已经讲完，没有高级用法，直接进入下一个Hook。&lt;/p&gt;
&lt;p&gt;不！基于react 16.13版本的全部 hook，终于讲完了，没有下一个hook了。&lt;/p&gt;
&lt;p&gt;能坚持到现在，真的不容易，默认自带的 react hook 学完后，还需要学习如何自定义hook...&lt;br /&gt;
扶我起来，再坚持一下。&lt;/p&gt;
</content:encoded></item><item><title>14 useLayoutEffect基础用法</title><link>https://blog.wemang.com/posts/web/react/uselayouteffect%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/uselayouteffect%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</guid><pubDate>Mon, 18 Sep 2023 15:57:04 GMT</pubDate><content:encoded>&lt;h1&gt;14 useLayoutEffect基础用法&lt;/h1&gt;
&lt;h2&gt;useLayoutEffect概念解释&lt;/h2&gt;
&lt;p&gt;我们第九个要学习的Hook(钩子函数)是useLayoutEffect，他的作用是“勾住”挂载或重新渲染完成这2个组件生命周期函数。useLayoutEffect使用方法、所传参数和useEffect完全相同。&lt;/p&gt;
&lt;p&gt;他们的不同点在于，你可以把useLayoutEffect等同于componentDidMount、componentDidUpdate，因为他们调用阶段是相同的。而useEffect是在componentDidMount、componentDidUpdate调用之后才会触发的。&lt;/p&gt;
&lt;p&gt;也就是说，当组件所有DOM都渲染完成后，同步调用useLayoutEffect，然后再调用useEffect。&lt;/p&gt;
&lt;p&gt;useLayoutEffect永远要比useEffect先触发完成。&lt;/p&gt;
&lt;p&gt;那通常在useLayoutEffect阶段我们可以做什么呢？&lt;br /&gt;
答：在触发useLayoutEffect阶段时，页面全部DOM已经渲染完成，此时可以获取当前页面所有信息，包括页面显示布局等，你可以根据需求修改调整页面。&lt;/p&gt;
&lt;p&gt;请注意，useLayoutEffect对页面的某些修改调整可能会触发组件重新渲染。如果是对DOM进行一些样式调整是不会触发重新渲染的，这点和useEffect是相同的。&lt;/p&gt;
&lt;p&gt;在react官方文档中，明确表示只有在useEffect不能满足你组件需求的情况下，才应该考虑使用useLayoutEffect。  官方推荐优先使用useEffect。&lt;/p&gt;
&lt;p&gt;请注意：如果是服务器渲染，无论useEffect还是useLayoutEffect 都无法在JS代码加载完成之前执行，因此都会收到错误警告。  服务器渲染时若想使用useEffect，解决方案不在本章中讨论。&lt;/p&gt;
&lt;p&gt;让我们回到useLayoutEffect基础学习中。&lt;/p&gt;
&lt;h2&gt;useLayoutEffect是来解决什么问题的？&lt;/h2&gt;
&lt;p&gt;答：useLayoutEffect的作用是“当页面挂载或渲染完成时，再给你一次机会对页面进行修改”。&lt;/p&gt;
&lt;p&gt;如果你选择使用useLayoutEffect，对页面进行了修改，更改样式不会引发重新渲染，但是修改变量则会触发再次渲染。&lt;br /&gt;
如果你不使用useLayoutEffect，那么之后就应该调用useEffect。&lt;/p&gt;
&lt;p&gt;补充说明：&lt;br /&gt;
1、优先使用useEffect，useEffect无法满足需求时再考虑使用useLayoutEffect。&lt;br /&gt;
2、useLayoutEffect先触发，useEffect后触发。&lt;br /&gt;
3、useEffect和useLayoutEffect在服务器端渲染时，都不行，需要寻求别的解决方案。&lt;/p&gt;
&lt;h2&gt;useLayoutEffect函数源码：&lt;/h2&gt;
&lt;p&gt;回到useLayoutEffect的学习中，首先看一下React源码中的&lt;a href=&quot;https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js&quot;&gt;ReactHooks.js&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //备注：源码采用TypeScript编写，如果不懂TS代码，阅读起来稍显困难
    export function useLayoutEffect(
      create: () =&amp;gt; (() =&amp;gt; void) | void,
      deps: Array&amp;lt;mixed&amp;gt; | void | null,
    ): void {
      const dispatcher = resolveDispatcher();
      return dispatcher.useLayoutEffect(create, deps);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码看不懂没关系，本系列教程只是讲述“如何使用Hook”，并不是“Hook源码分析”。^_^  你只需知道useLayoutEffect的用法和useEffect一模一样即可。&lt;/p&gt;
&lt;h2&gt;useLayoutEffect基本用法&lt;/h2&gt;
&lt;p&gt;useLayoutEffect的用法和useEffect的用法相同，所以不再阐述。&lt;/p&gt;
&lt;h2&gt;useLayoutEffect使用示例：&lt;/h2&gt;
&lt;p&gt;请原谅，目前竟然找不到一个useLayoutEffect合适的例子，因为能够想到的应用场景其实都可以用useEffect来代替。&lt;/p&gt;
&lt;p&gt;那只能贴出一段简单的代码，让你看确认一下，useLayoutEffect先于useEffect触发调用。&lt;/p&gt;
&lt;p&gt;代码示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{useState,useEffect,useLayoutEffect} from &apos;react&apos;

    function LayoutEffect() {
      const [count,setCount] = useState(0);

      useEffect(() =&amp;gt; {
        console.log(&apos;useEffect...&apos;);
      },[count]);

      useLayoutEffect(() =&amp;gt; {
        console.log(&apos;useLayoutEffect...&apos;);
      },[count]);

      return (
        &amp;lt;div&amp;gt;
            {count}
            &amp;lt;button onClick={() =&amp;gt; {setCount(count+1)}}&amp;gt;Click&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }
    export default LayoutEffect
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际运行就会发现：&lt;br /&gt;
无论是首次挂载，还是重新渲染，console面板中，输出顺序都是&lt;br /&gt;
useLayoutEffect...&lt;br /&gt;
useEffect...&lt;/p&gt;
&lt;p&gt;也就确认，先执行useLayoutEffect，后执行useEffect。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useLayoutEffect基础用法已经讲完，没有高级用法，直接进入下一个Hook。&lt;/p&gt;
</content:encoded></item><item><title>13 useImperativeHandle基础用法</title><link>https://blog.wemang.com/posts/web/react/useimperativehandle%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/useimperativehandle%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</guid><pubDate>Wed, 13 Sep 2023 14:48:20 GMT</pubDate><content:encoded>&lt;h1&gt;13 useImperativeHandle基础用法&lt;/h1&gt;
&lt;h2&gt;useImperativeHandle概念解释&lt;/h2&gt;
&lt;p&gt;我们第八个要学习的Hook(钩子函数)是useImperativeHandle，他的作用是“勾住”子组件中某些函数(方法)供父组件调用。&lt;/p&gt;
&lt;p&gt;先回顾一下之前学到的。&lt;br /&gt;
第1个知识点：&lt;br /&gt;
react属于单向数据流，父组件可以通过属性传值，将父组件内的函数(方法)传递给子组件，实现子组件调用父组件内函数的目的。&lt;/p&gt;
&lt;p&gt;第2个知识点：&lt;br /&gt;
1、useRef 可以“勾住”某些本组件挂载完成或重新渲染完成后才拥有的某些对象。&lt;br /&gt;
2、React.forwardRef 可以“勾住”某些子组件挂载完成或重新渲染完成后才拥有的某些对象。&lt;br /&gt;
上面无论哪种情况，由于勾住的对象都是渲染后的原生html对象，父组件只能通过ref调用该原生html对象的函数(方法)。&lt;/p&gt;
&lt;p&gt;如果父组件想调用子组件中自定义的方法，该怎么办？&lt;br /&gt;
答：使用useImperativeHandle()。&lt;/p&gt;
&lt;p&gt;让我们回到useImperativeHandle基础学习中。&lt;/p&gt;
&lt;h2&gt;useImperativeHandle是来解决什么问题的？&lt;/h2&gt;
&lt;p&gt;答：useImperativeHandle可以让父组件获取并执行子组件内某些自定义函数(方法)。本质上其实是子组件将自己内部的函数(方法)通过useImperativeHandle添加到父组件中useRef定义的对象中。&lt;/p&gt;
&lt;p&gt;补充说明：&lt;br /&gt;
1、useRef创建引用变量&lt;br /&gt;
2、React.forwardRef将引用变量传递给子组件&lt;br /&gt;
3、useImperativeHandle将子组件内定义的函数作为属性，添加到父组件中的ref对象上。&lt;/p&gt;
&lt;p&gt;因此，如果想使用useImperativeHandle，那么还要结合useRef、React.forwardRef一起使用。&lt;/p&gt;
&lt;h2&gt;useImperativeHandle函数源码：&lt;/h2&gt;
&lt;p&gt;回到useImperativeHandle的学习中，首先看一下React源码中的&lt;a href=&quot;https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js&quot;&gt;ReactHooks.js&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //备注：源码采用TypeScript编写，如果不懂TS代码，阅读起来稍显困难
    export function useImperativeHandle&amp;lt;T&amp;gt;(
      ref: {|current: T | null|} | ((inst: T | null) =&amp;gt; mixed) | null | void,
      create: () =&amp;gt; T,
      deps: Array&amp;lt;mixed&amp;gt; | void | null,
    ): void {
      const dispatcher = resolveDispatcher();
      return dispatcher.useImperativeHandle(ref, create, deps);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码看不懂没关系，本系列教程只是讲述“如何使用Hook”，并不是“Hook源码分析”。^_^&lt;/p&gt;
&lt;h2&gt;useImperativeHandle基本用法&lt;/h2&gt;
&lt;p&gt;useImperativeHandle(ref,create,[deps])函数前2个参数为必填项，第3个参数为可选项。&lt;br /&gt;
第1个参数为父组件通过useRef定义的引用变量；&lt;br /&gt;
第2个参数为子组件要附加给ref的对象，该对象中的属性即子组件想要暴露给父组件的函数(方法)；&lt;br /&gt;
第3个参数为可选参数，为函数的依赖变量。凡是函数中使用到的数据变量都需要放入deps中，如果处理函数没有任何依赖变量，可以忽略第3个参数。&lt;/p&gt;
&lt;p&gt;请注意：&lt;br /&gt;
1、这里面说的“勾住子组件内自定义函数”本质上是子组件将内部自定义的函数添加到父组件的ref.current上面。&lt;br /&gt;
2、父组件若想调用子组件暴露给自己的函数，可以通过 res.current.xxx 来访问或执行。&lt;/p&gt;
&lt;h5&gt;代码形式：&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;    const xxx = () =&amp;gt; {
        //do smoting...
    }
    useImperativeHandle(ref,() =&amp;gt; ({xxx}));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码中，&lt;code&gt;useImperativeHandle(ref,() =&amp;gt; ({xxx}))&lt;/code&gt; 其实是 &lt;code&gt;useImperativeHandle(ref,() =&amp;gt; {return {xxx:xxx}})&lt;/code&gt;的简写。&lt;/p&gt;
&lt;p&gt;特别注意：&lt;code&gt;() =&amp;gt; ({xxx})&lt;/code&gt; 不可以再简写成 &lt;code&gt;() =&amp;gt; {xxx}&lt;/code&gt;，如果这样写会直接react报错。&lt;br /&gt;
因为这两种写法意思完全不一样：&lt;br /&gt;
1、&lt;code&gt;() =&amp;gt; ({xxx})&lt;/code&gt; 表示 返回一个object对象，该对象为&lt;code&gt;{xxx}&lt;/code&gt;
2、&lt;code&gt;() =&amp;gt; {xxx}&lt;/code&gt; 表示 执行 xxx 语句代码&lt;/p&gt;
&lt;h5&gt;拆解说明：&lt;/h5&gt;
&lt;p&gt;1、子组件内部先定义一个 xxx 函数&lt;br /&gt;
2、通过useImperativeHandle函数，将 xxx函数包装成一个对象，并将该对象添加到父组件内部定义的ref中。&lt;br /&gt;
3、若 xxx 函数中使用到了子组件内部定义的变量，则还需要将该变量作为 依赖变量 成为useImperativeHandle第3个参数，上面示例中则选择忽略了第3个参数。&lt;br /&gt;
4、若父组件需要调用子组件内的 xxx函数，则通过：res.current.xxx()。&lt;br /&gt;
5、请注意，该子组件在导出时必须被 React.forwardRef()包裹住才可以。&lt;/p&gt;
&lt;h2&gt;useImperativeHandle使用示例：&lt;/h2&gt;
&lt;p&gt;举例，若某子组件的需求为：&lt;br /&gt;
1、有变量count，默认值为0&lt;br /&gt;
2、有一个函数 addCount，该函数体内部执行 count+1&lt;br /&gt;
3、有一个按钮，点击按钮执行 addCount 函数&lt;/p&gt;
&lt;p&gt;父组件的需求为：&lt;br /&gt;
1、父组件内使用上述子组件&lt;br /&gt;
2、父组件内有一个按钮，点击执行上述子组件内定义的函数 addCount&lt;/p&gt;
&lt;p&gt;子组件的代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{useState,useImperativeHandle} from &apos;react&apos;

    function ChildComponent(props,ref) {
      const [count,setCount] =  useState(0); //子组件定义内部变量count
      //子组件定义内部函数 addCount
      const addCount = () =&amp;gt; {
        setCount(count + 1);
      }
      //子组件通过useImperativeHandle函数，将addCount函数添加到父组件中的ref.current中
      useImperativeHandle(ref,() =&amp;gt; ({addCount}));
      return (
        &amp;lt;div&amp;gt;
            {count}
            &amp;lt;button onClick={addCount}&amp;gt;child&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }

    //子组件导出时需要被React.forwardRef包裹，否则无法接收 ref这个参数
    export default React.forwardRef(ChildComponent);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;父组件的代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{useRef} from &apos;react&apos;
    import ChildComponent from &apos;./childComponent&apos;

    function Imperative() {
      const childRef = useRef(null); //父组件定义一个对子组件的引用

      const clickHandle = () =&amp;gt; {
        childRef.current.addCount(); //父组件调用子组件内部 addCount函数
      }

      return (
        &amp;lt;div&amp;gt;
            {/* 父组件通过给子组件添加 ref 属性，将childRef传递给子组件，
                子组件获得该引用即可将内部函数添加到childRef中 */}
            &amp;lt;ChildComponent ref={childRef} /&amp;gt;
            &amp;lt;button onClick={clickHandle}&amp;gt;child component do somting&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }

    export default Imperative;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;思考一下真的有必要使用useImperativeHandle吗？&lt;/h4&gt;
&lt;p&gt;从实际运行的结果，无论点击子组件还是父组件内的按钮，都将执行 addCount函数，使 count+1。&lt;/p&gt;
&lt;p&gt;react为单向数据流，如果为了实现这个效果，我们完全可以把需求转化成另外一种说法，即：&lt;br /&gt;
1、父组件内定义一个变量count 和 addCount函数&lt;br /&gt;
2、父组件把 count 和 addCount 通过属性传值 传递给子组件&lt;br /&gt;
3、点击子组件内按钮时调用父组件内定义的 addCount函数，使 count +1。&lt;/p&gt;
&lt;p&gt;你会发现即使把需求中的 父与子组件 描述对调一下，“最终实际效果”是一样的。&lt;/p&gt;
&lt;p&gt;所以，到底使用哪种形式，需要根据组件实际需求来做定夺。&lt;/p&gt;
&lt;h4&gt;说一个有点绕的情况&lt;/h4&gt;
&lt;p&gt;子组件导出时：&lt;br /&gt;
1、假设某个子组件为了提高性能，导出时需要用React.memo包裹。&lt;br /&gt;
2、可是他也需要暴露自己内部函数给父组件，导出时也需要用React.forwardRef包裹。&lt;/p&gt;
&lt;p&gt;子组件内部函数：&lt;br /&gt;
1、假设该组件内部函数为了性能，需要用到 useCallback包裹该函数。&lt;br /&gt;
2、同时为了让将该函数暴露给父级，也需要用 useImperativeHandle包裹。&lt;/p&gt;
&lt;p&gt;呵，该怎么办，层层包裹吗？  虽然性能提升了，可是那样的代码可读性还有多少，怎么办？&lt;br /&gt;
答：不知道，反正本系列文章只是单独来讲解某个hook怎么使用，这种复杂包裹的情况，你自己看着办吧。&lt;/p&gt;
&lt;p&gt;事实上这种情况出现的几率非常小，当我们开发react组件时，不应该为了使用某hook而使用。还是应该是依据单向数据流的原则来做设计方案。&lt;br /&gt;
就好像本章中示例代码，其实完全可以不用useImperativeHandle，而是继续使用最常见的父组件属性传值给子组件的方式。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useImperativeHandle基础用法已经讲完，没有高级用法，直接进入下一个Hook。&lt;/p&gt;
</content:encoded></item><item><title>12 useRef基础用法</title><link>https://blog.wemang.com/posts/web/react/useref%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/useref%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</guid><pubDate>Tue, 12 Sep 2023 14:48:15 GMT</pubDate><content:encoded>&lt;h1&gt;12 useRef基础用法&lt;/h1&gt;
&lt;h2&gt;useRef概念解释&lt;/h2&gt;
&lt;p&gt;我们第七个要学习的Hook(钩子函数)是useRef，他的作用是“勾住”某些组件挂载完成或重新渲染完成后才拥有的某些对象，并返回该对象的引用。该引用在组件整个生命周期中都固定不变，该引用并不会随着组件重新渲染而失效。&lt;/p&gt;
&lt;p&gt;上面这段话，就算你认真读几遍，估计也是一头雾水，到底说的是啥？&lt;br /&gt;
我也实在想不出其他更加通俗的语言来描述useRef，不过经过下面的详细分解描述，相信能帮到你来理解useRef。&lt;/p&gt;
&lt;h5&gt;“某些组件挂载完成或重新渲染完成后才拥有的某些对象”&lt;/h5&gt;
&lt;p&gt;这句话中的“某些对象”主要分为3种：JSX组件转换后对应的真实DOM对象、在useEffect中创建的变量、子组件内自定义的函数(方法)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第1：JSX组件转换后对应的真实DOM对象&lt;/strong&gt;：&lt;br /&gt;
举例：假设在JSX中，有一个输入框&lt;code&gt;&amp;lt;input type=&apos;text&apos; /\&amp;gt;&lt;/code&gt;，这个标签最终将编译转换成真正的html标签中的&lt;code&gt;&amp;lt;input type=&apos;text&apos;/\&amp;gt;&lt;/code&gt;。&lt;br /&gt;
你应该知道以下几点：&lt;br /&gt;
1、JSX中小写开头的组件看似和原生html标签相似，但是并不是真的原生标签，依然是react内置组件。&lt;br /&gt;
2、什么时候转换？ 虚拟DOM转化为真实DOM
3、什么时候可访问？组件挂载完成或重新渲染完成后&lt;/p&gt;
&lt;p&gt;对于上面举例中的那个转换后的&lt;code&gt;&amp;lt;input/\&amp;gt;&lt;/code&gt; 真实DOM，只有组件挂载完成或重新渲染完成后才可以访问，它就就属于“某些组件挂载完成或重新渲染完成后才拥有的某些对象”。&lt;/p&gt;
&lt;p&gt;特别强调：useRef只适合“勾住”小写开头的类似原生标签的组件。如果是自定义的react组件(自定义的组件必须大写字母开头)，那么是无法使用useRef的。&lt;/p&gt;
&lt;p&gt;思考：如何获取这个 &lt;code&gt;&amp;lt;input/\&amp;gt;&lt;/code&gt; 真实DOM呢？&lt;br /&gt;
答：用useRef。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第2：在useEffect中创建的变量&lt;/strong&gt;：&lt;br /&gt;
举例，请看以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    useEffect(() =&amp;gt; {
        let timer = setInterval(() =&amp;gt; {
            setCount(prevData =&amp;gt; prevData +1);
        }, 1000);
        return () =&amp;gt; {
            clearInterval(timer);
        }
    },[]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码中，请注意这个timer是在useEffect中才定义的。&lt;/p&gt;
&lt;p&gt;思考：useEffect 以外的地方，该如何获取这个 timer 的引用？&lt;br /&gt;
答：用useRef&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第3：子组件内自定义的函数(方法)&lt;/strong&gt;&lt;br /&gt;
由于需要结合useImperativeHandle才可以实现，而useImperativeHandle目前还未学习，所以本章中不讨论这个怎么实现。&lt;br /&gt;
本章只讲前2中应用场景。&lt;/p&gt;
&lt;h5&gt;“并返回该对象的引用”&lt;/h5&gt;
&lt;p&gt;上面的前2种情况，都提到用useRef来获取对象的引用。具体如何获取，稍后在useRef用法中会有演示。&lt;/p&gt;
&lt;h5&gt;“该引用在组件整个生命周期中都固定不变”&lt;/h5&gt;
&lt;p&gt;假设通过useRef获得了该对象的引用，那么当react组件重新渲染后，如何保证该引用不丢失？&lt;br /&gt;
答：react在底层帮我们做了这个工作，我们只需要相信之前的引用可以继续找到目标对象即可。&lt;/p&gt;
&lt;p&gt;请注意：React.createRef()也有useRef相似效果，但是React.createRef无法全部适用上面提到的3种情况。&lt;/p&gt;
&lt;p&gt;让我们回到useRef基础学习中。&lt;/p&gt;
&lt;h2&gt;useRef是来解决什么问题的？&lt;/h2&gt;
&lt;p&gt;答：useRef可以“获取某些组件挂载完成或重新渲染完成后才拥有的某些对象”的引用，且保证该引用在组件整个生命周期内固定不变，都能准确找到我们要找的对象。&lt;br /&gt;
具体已经在useRef中做了详细阐述，这里不再重复。&lt;/p&gt;
&lt;p&gt;补充说明：&lt;br /&gt;
1、useRef是针对函数组件的，如果是类组件则使用React.createRef()。
2、React.createRef()也可以在函数组件中使用。&lt;br /&gt;
只不过React.createRef创建的引用不能保证每次重新渲染后引用固定不变。如果你只是使用React.createRef“勾住”JSX组件转换后对应的真实DOM对象是没问题的，但是如果想“勾住”在useEffect中创建的变量，那是做不到的。&lt;/p&gt;
&lt;p&gt;2者都想可以“勾住”，只能使用useRef。&lt;/p&gt;
&lt;h2&gt;注意注意&lt;/h2&gt;
&lt;p&gt;在后面useImperativeHandle的学习中，你会知道useRef还可以 “勾住并调用” 子组件内定义的函数(方法)。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下内容更新于 2022.04.06&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;特别注意：修改 .current 的值并不会触发组件重新渲染&lt;/h2&gt;
&lt;p&gt;在本文开头介绍 useRef  时用了这句话 “useRef 是“勾住”某些组件挂载完成或重新渲染完成后才拥有的某些对象，并返回该对象的引用。”&lt;/p&gt;
&lt;p&gt;也就是说 &lt;strong&gt;先有了 组件渲染，之后才更新了 useRef 中 .current 的值。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;也就是说 useRef 变量的 current 的值实际上是 组件渲染 后的一个副产品。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;这句话暗含了另外一层含义：主动更新 useRef 变量的 .current 的值并不会触发组件重新渲染。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;例如下面这个示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useRef } from &quot;react&quot;;

export default function MyButton() {
  const countRef = useRef(0)

  const handleClick = () =&amp;gt; {
    countRef.current = countRef.current + 1
  };

  return &amp;lt;button onClick={handleClick}&amp;gt;Click me {countRef.current}&amp;lt;/button&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际运行就会发现，在点击事件中我们修改了 countRef.current 的值，尽管该值确实发生了变化，可是并不会触发组件的重新渲染。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 useState() 产生的变量值发生变化后，是会触发组件重新渲染的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;以上内容更新于 2022.04.06&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;useRef函数源码&lt;/h2&gt;
&lt;p&gt;回到useRef的学习中，首先看一下React源码中的&lt;a href=&quot;https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js&quot;&gt;ReactHooks.js&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //备注：源码采用TypeScript编写，如果不懂TS代码，阅读起来稍显困难
    
    export function useRef&amp;lt;T&amp;gt;(initialValue: T): {|current: T|} {
      const dispatcher = resolveDispatcher();
      return dispatcher.useRef(initialValue);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码看不懂没关系，本系列教程只是讲述“如何使用Hook”，并不是“Hook源码分析”。^_^&lt;/p&gt;
&lt;h2&gt;useRef基本用法&lt;/h2&gt;
&lt;p&gt;useRef(initialValue)函数只有1个可选参数，该参数为默认“勾住”的对象。绝大多数实际的情况是，默认“勾住”的对象在JSX未编译前(组件挂载或重新渲染后)根本不存在，所以更多时候都会传一个 null 作为默认值。如果不传任何参数，那么react默认将参数设置为undefined。&lt;/p&gt;
&lt;p&gt;就目前本人所理解的，日常使用过程中useRef(null)和useRef() 实际上是没有什么区别的。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;以下更新于 2020.12.10&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;补充一下 React + TypeScript 知识点：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;useRef(null) 和 useRef() 在 React + TypeScript 项目中还是有差别的。&lt;/p&gt;
&lt;p&gt;假设我们要勾住一个 &lt;code&gt;&amp;lt;canvas /\&amp;gt;&lt;/code&gt; DOM元素，那么：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const canvasRef1 = useRef&amp;lt;HTMLCanvasElement&amp;gt;(null)
const canvasRef2 = useRef&amp;lt;HTMLCanvasElement&amp;gt;()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;canvasRef1.current 的类型为：HTMLCanvasElement | null&lt;/li&gt;
&lt;li&gt;canvasRef2.current 的类型为：HTMLCanvasElement | null | undefined&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以上更新于 2020.12.10&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;第2遍强调：本文提到的组件，默认都是指小写开头的类似原生标签的组件，不可以是自定义组件。&lt;/p&gt;
&lt;p&gt;接下来具体说说useRef关联对象的2种用法：&lt;br /&gt;
1、针对 JSX组件，通过属性 &lt;code&gt;ref={xxxRef}&lt;/code&gt; 进行关联。&lt;br /&gt;
2、针对 useEffect中的变量，通过 xxxRef.current 进行关联。&lt;/p&gt;
&lt;h5&gt;代码形式&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;    //先定义一个xxRef引用变量，用于“勾住”某些组件挂载完成或重新渲染完成后才拥有的某些对象
    const xxRef = useRef(null);
    
    //针对 JSX组件，通过属性 ref={xxxRef} 进行关联
    &amp;lt;xxx ref={xxRef} /&amp;gt;
    
    //针对 useEffect中的变量，通过 xxxRef.current 进行关联
    useEffect(() =&amp;gt; {
       xxRef.current = xxxxxx;
    },[]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;拆解说明&lt;/h5&gt;
&lt;p&gt;1、具体讲解已在上面示例代码中做了多项注释，此处不再重复；&lt;/p&gt;
&lt;h4&gt;&apos;ref&apos;补充说明&lt;/h4&gt;
&lt;p&gt;1、组件的 ref 为特殊属性名，他并不存在组件属性传值的 props 中。&lt;br /&gt;
2、如果给一个组件设定了 ref 属性名，但是对应的值却不是由 useRef 创建的，那么实际运行中会收到react的报错，无法正常渲染。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;&amp;lt;xxx&amp;gt;&lt;/code&gt;补充说明&lt;/h4&gt;
&lt;p&gt;1、useRef只能针对react中小写开头的类似原生标签的组件，所以这里用的是 &lt;code&gt;&amp;lt;xxx&amp;gt;&lt;/code&gt; 而不是 &lt;code&gt;&amp;lt;Xxx&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;&apos;xxxRef.current&apos;补充说明&lt;/h4&gt;
&lt;p&gt;1、当需要使用“勾住”的对象时，也是通过xxRef.current来获取该对象的。&lt;/p&gt;
&lt;h2&gt;useRef使用示例1&lt;/h2&gt;
&lt;p&gt;若我们有一个组件，该组件只有一个输入框，我们希望当该组件挂载到网页后，自动获得输入焦点。&lt;/p&gt;
&lt;p&gt;需求分析：&lt;br /&gt;
1、我们可以很轻松使用&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;创建出这个输入框。&lt;br /&gt;
2、需要使用useRef “勾住”这个输入框，当它被挂载到网页后，通过操作原生html的方法，将焦点赋予该输入框上。&lt;/p&gt;
&lt;p&gt;完整代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{useEffect,useRef} from &apos;react&apos;
    
    function Component() {
      //先定义一个inputRef引用变量，用于“勾住”挂载网页后的输入框
      const inputRef = useRef(null);
    
      useEffect(() =&amp;gt; {
        //inputRef.current就是挂载到网页后的那个输入框，一个真实DOM，因此可以调用html中的方法focus()
        inputRef.current.focus();
      },[]);
    
      return &amp;lt;div&amp;gt;
          {/* 通过 ref 属性将 inputRef与该输入框进行“挂钩” */}
          &amp;lt;input type=&apos;text&apos; ref={inputRef} /&amp;gt;
        &amp;lt;/div&amp;gt;
    }
    export default Component
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;br /&gt;
1、在给组件设置 ref 属性时，只需传入 inputRef，千万不要传入 inputRef.current。&lt;br /&gt;
2、在“勾住”渲染后的真实DOM输入框后，能且只能调用原生html中该标签拥有的方法。&lt;/p&gt;
&lt;h2&gt;useRef使用示例2&lt;/h2&gt;
&lt;p&gt;若我们有一个组件，该组件的功能需求如下：&lt;br /&gt;
1、组件中有一个变量count，当该组件挂载到网页后，count每秒自动 +1。&lt;br /&gt;
2、组件中有一个按钮，点击按钮可以停止count自动+1。&lt;/p&gt;
&lt;p&gt;需求分析：&lt;br /&gt;
1、声明内部变量count用 useState&lt;br /&gt;
2、可以在useEffect 通过setInterval创建一个计时器timer，实现count每秒自动 +1&lt;br /&gt;
3、当组件卸载前，需要通过 clearInterval 将timer清除&lt;br /&gt;
4、按钮点击处理函数中，也要通过 clearInterval 将timer清除&lt;/p&gt;
&lt;p&gt;假设我们不使用useRef，那该如何实现？&lt;/p&gt;
&lt;p&gt;为了确保timer可以被useEffect以外地方也能访问，我们通常做法是将timer声明提升到useEffect以外。&lt;br /&gt;
代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{useState,useEffect} from &apos;react&apos;
    
    function Component() {
      const [count,setCount] = useState(0);
      const [timer,setTimer] = useState(null); //单独声明定义timer，目的是为了让组件内所有地方都可以访问到timer
    
      useEffect(() =&amp;gt; {
        //需要用setTimer()包裹住 setInterval()
        setTimer(setInterval(() =&amp;gt; {
            setCount((prevData) =&amp;gt; {return prevData +1});
        }, 1000));
        return () =&amp;gt; {
          //清除掉timer
          clearInterval(timer);
        }
      },[]);
    
      const clickHandler = () =&amp;gt; {
        //清除掉timer
        clearInterval(timer);
      };
    
      return (
        &amp;lt;div&amp;gt;
            {count}
            &amp;lt;button onClick={clickHandler} &amp;gt;stop&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }
    
    export default Component
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用useRef，该如何实现？
代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{useState,useEffect,useRef} from &apos;react&apos;
    
    function Component() {
      const [count,setCount] =  useState(0);
      const timerRef = useRef(null);//先定义一个timerRef引用变量，用于“勾住”useEffect中通过setIntervale创建的计时器
    
      useEffect(() =&amp;gt; {
        //将timerRef.current与setIntervale创建的计时器进行“挂钩”
        timerRef.current = setInterval(() =&amp;gt; {
            setCount((prevData) =&amp;gt; { return prevData +1});
        }, 1000);
        return () =&amp;gt; {
            //通过timerRef.current，清除掉计时器
            clearInterval(timerRef.current);
        }
      },[]);
    
      const clickHandler = () =&amp;gt; {
        //通过timerRef.current，清除掉计时器
        clearInterval(timerRef.current);
      };
    
      return (
        &amp;lt;div&amp;gt;
            {count}
            &amp;lt;button onClick={clickHandler} &amp;gt;stop&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }
    
    export default Component
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;两种实现方式对比：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1、两种实现方式最主要的差异地方在于 如何创建组件内对计时器的引用。&lt;br /&gt;
2、两种创建引用的方式，分别是：用useState创建的timer、用useRef创建的timerRef&lt;br /&gt;
3、在使用setInterval时，相对来说timerRef.current更加好用简单，结构清晰，不需要像 setTimer那样需要再多1层包裹。&lt;br /&gt;
4、timer更像是一种react对计时器的映射，而timerRef直接就是真实DOM中计时器的引用，timerRef能够调用更多的原生html中的JS方法和属性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结论：&lt;/strong&gt;&lt;br /&gt;
1、如果需要对渲染后的DOM节点进行操作，必须使用useRef。&lt;br /&gt;
2、如果需要对渲染后才会存在的变量对象进行某些操作，建议使用useRef。&lt;/p&gt;
&lt;p&gt;第3遍强调：useRef只适合“勾住”小写开头的类似原生标签的组件。如果是自定义的react组件(自定义的组件必须大写字母开头)，那么是无法使用useRef的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下内容更新于 2022.05.20&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;useRef使用示例3：父组件调用子组件中的函数&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;首先特别强调：除非情况非常特殊，否则一般情况下都不要采用 父组件调用子组件的函数 这种策略。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用 useRef 实现父组件调用子组件中的函数 实现思路：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;父组件中通过 useRef 定义一个钩子变量，例如 childFunRef&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;父组件通过参数配置，将 childFunRef 传递给子组件&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;子组件在自己的 useEffect() 中定义一个函数，例如 doSomting()&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;划重点：一定要在 useEffect() 中定义 doSomting()，不能直接在子组件内部定义。&lt;/p&gt;
&lt;p&gt;因为如果 doSomting() 定义在子组件内部，那么就会造成每一次组件刷新都会重新生成一份 doSomthing()&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后将 doSomting() 赋值到 childFunRef.current 中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这样，当父组件想调用子组件中的 doSomting() 时，可执行 childFunRef.current.doSomting()&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;具体示例代码：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ParentComponent&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useRef } from &quot;react&quot;;
import ChildComponent from &quot;./child&quot;;

const ParentComponent = () =&amp;gt; {
  const childFunRef = useRef();
  const handleOnClick = () =&amp;gt; {
    if (childFunRef.current) {
      childFunRef.current.doSomething();
    }
  };
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;ChildComponent funRef={childFunRef} /&amp;gt;
      &amp;lt;button onClick={handleOnClick}&amp;gt;执行子项的doSomething()&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default ParentComponent;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;ChildComponent&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useEffect, useState } from &quot;react&quot;;

const ChildComponent = ({ funRef }) =&amp;gt; {
  const [num, setNum] = useState(0);
  useEffect(() =&amp;gt; {
    const doSomething = () =&amp;gt; {
      setNum(Math.floor(Math.random() * 100));
    };
    funRef.current = { doSomething }; //在子组件中修改父组件中定义的childFunRef的值
  }, [funRef]);
  return &amp;lt;div&amp;gt;{num}&amp;lt;/div&amp;gt;;
};

export default ChildComponent;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;特别说明：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;下一章要讲解的 useImperativeHandle 也是用来实现 父组件调用子组件内定义的函数的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再次强调，如非必要，真的不要使用 父组件调用子组件内函数 这种策略。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;最近我遇到了一个需求，子组件是一个第三方写好的轮播图，父组件需要调用这个轮播图的 next() 的函数来切换下一张，所以才使用了这种策略。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;以上内容更新于 2022.05.20&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;以下内容更新于2020.11.18&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;在 TypeScript 中使用 useRef 创建计时器注意事项&lt;/h4&gt;
&lt;p&gt;在上面代码示例中，请注意这一行代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;timerRef.current = setInterval(() =&amp;gt; {
        setCount((prevData) =&amp;gt; { return prevData +1});
    }, 1000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是在 TS 语法下，上面的代码会报错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;不能将类型“Timeout”分配给类型“number”。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Timeout ???&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;造成这个错误提示的原因是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;TypeScript 是运行在 Nodejs 环境下的，TS 编译之后的代码是运行在浏览器环境下的。&lt;/li&gt;
&lt;li&gt;Nodejs 和浏览器中的 window 他们各自实现了一套自己的 setInterval&lt;/li&gt;
&lt;li&gt;原来代码 timerRef.current = setInterval( ... ) 中 setInterval 会被 TS 认为是 Nodejs 定义的 setInterval，而  Nodejs 中 setInterval 返回的类型就是 NodeJS.Timeout。&lt;/li&gt;
&lt;li&gt;所以，我们需要将上述代码修改为：timerRef.current = window.setInterval( ... )，明确我们调用的是 window.setInterval，而不是 Nodejs 的 setInterval。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;附一个 TS 代码示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { useRef, useEffect } from &apos;react&apos;

const MyTemp = () =&amp;gt; {
    const timer = useRef&amp;lt;number | undefined&amp;gt;()

    useEffect(() =&amp;gt; {
        timer.current = window.setInterval(() =&amp;gt; {
            console.log(0)
        }, 1000)

        return () =&amp;gt; {
            clearInterval(timer.current)
        }
    }, [])
    return (
        &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
    )
}

export default MyTemp
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;以上内容更新于2020.11.18&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;以下内容更新于2020.12.03&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;在 TypeScript 中给 useRef.current 赋值的注意事项&lt;/h4&gt;
&lt;p&gt;在 jsx 文件中，以下代码是不会有问题的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const myRef = useRef(null)

myRef.current = xxxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，在我们使用 TypeScript 之后，按照习惯改成以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const myRef = useRef&amp;lt;Xxx&amp;gt;(null)

myRef.current = xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，会收到 TypeScript 的报错：&lt;strong&gt;无法分配到 &quot;current&quot; ，因为 myRef.current 是只读属性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;报错原因：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;React 的作者并没有规定使用 useRef(null) 之后 myRef.current 就不可以再修改了。&lt;/p&gt;
&lt;p&gt;但是 TypeScript 的作者认为，若使用 useRef(null) 之后，myRef 就应该交由 React 来托管，外界不应该有权利去修改 myRef.current，因此此时会把 myRef.current 当做只读属性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方式：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解决方式1：不给 useRef 设置 null 这个默认值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const myRef = useRef&amp;lt;Xxx&amp;gt;()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决方式2：就是将原本的类型定义，修改成以下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const myRef = useRef&amp;lt;Xxx | null&amp;gt;(null)

//或者是

const myRef = useRef&amp;lt;Xxx | undefined&amp;gt;()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;myRef.current 的数据类型，除了 Xxx 之外，再加上 null 或 undefined ，这样 TypeScript 就认为  myRef.current 可能中途会发生修改，因此不会再将其设置为只读属性，此时再去执行 &lt;code&gt;myRef.current = xxx&lt;/code&gt; 不再会报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;验证一下：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们再去看看上面 2020.11.18 更新的 TypeScript 代码示例中：&lt;/p&gt;
&lt;p&gt;由于将来需要执行 timer.current =  window.setInterval ( ... )，也就是说需要给 timer.current 赋值。&lt;/p&gt;
&lt;p&gt;所以在定义时就使用以下方式，以确保 timer.current 不会被 TS 认为是只读属性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const timer = useRef&amp;lt;number | undefined&amp;gt;()
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;以上内容更新于2020.12.03&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;那如何“勾住”自定义组件中的“小写开头的类似原生标签的组件”？&lt;/h2&gt;
&lt;p&gt;答：使用React.forwardRef()。&lt;/p&gt;
&lt;h5&gt;你是否思考过这个问题：自定义组件到底是什么？&lt;/h5&gt;
&lt;p&gt;首先看一下“小写开头的类似原生标签的组件”，例如&lt;code&gt;&amp;lt;button\&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;input \&amp;gt;&lt;/code&gt;，我们很容易理解他是react内置的类似原生DOM的组件，最终都将直接转换成对应的真实DOM。&lt;/p&gt;
&lt;p&gt;那自定义组件又该如何理解，如何定义呢？&lt;/p&gt;
&lt;p&gt;假设我们有一个自定义组件 &lt;code&gt;&amp;lt;MyComponent\&amp;gt;&lt;/code&gt;，那么有以下几点是可以肯定的：&lt;br /&gt;
1、&lt;code&gt;&amp;lt;MyComponent\&amp;gt;&lt;/code&gt;内部return出去的，可以是小写开头的类似原生标签的组件，也可以是其他自定义组件。
2、无论嵌套多少次，最底层组件return出去的，一定是小写开头的类似原生标签的组件。&lt;br /&gt;
3、&lt;code&gt;&amp;lt;MyComponent\&amp;gt;&lt;/code&gt;内部一定创建了变量、处理函数等等。&lt;br /&gt;
4、挂载或渲染后的实际网页中，并不会存在&lt;code&gt;&amp;lt;MyComponent\&amp;gt;&lt;/code&gt;这个标签，存在的依然是各种原生html标签。&lt;/p&gt;
&lt;p&gt;为了简化更加容易理解，暂时姑且先把“小写开头的类似原生标签的组件”直接当做“原生html标签”。&lt;br /&gt;
那么“自定义组件”和“原生html标签”究竟区别在哪里呢？&lt;/p&gt;
&lt;p&gt;先不回答这个问题，再说另外一个问题：交互式html页面都有哪些构成？&lt;br /&gt;
答：有各种html标签 + JS对象(JS中定义的变量和函数)&lt;/p&gt;
&lt;p&gt;那我们使用react开发页面，“原生html标签”有了，那还缺什么？ 当然是 JS对象(JS中定义的变量和函数)。&lt;/p&gt;
&lt;p&gt;再回顾一下问题：“自定义组件”和“原生html标签”究竟区别在哪里呢？&lt;br /&gt;
答：“自定义组件”除了拥有“原生html标签”，还拥有JS对象(JS中定义的变量和函数)。&lt;/p&gt;
&lt;p&gt;再回顾一下开始的疑问：自定义组件到底是什么？&lt;br /&gt;
答：其实根本不存在自定义组件，所谓自定义组件，只不过是react给我们的各种语法糖，react并没有创造另外一门语言，react整体就是原生JS的语法糖。&lt;/p&gt;
&lt;p&gt;JSX语法 + Hook 组合起来，形成一个强大的语法糖，让你编写html标签和JS更加简便而已。
语法糖的对象就是：原生html标签 + JS对象(JS中定义的变量和函数)。&lt;/p&gt;
&lt;p&gt;这就解释了为啥自定义组件 return 出去的内容，最外层必须有一个原生html标签。 说白了，无论你怎么定义，折腾这个自定义组件，本质上都要保证这个自定义组件最终都能转换成一段原生html代码。&lt;/p&gt;
&lt;p&gt;上面这一大段话都是“简单到不能再简单的道理”，但是你只有理解透这一层，理解自定义组件、理解react究竟是什么之后，你会对于学习React各种API和Hook才会更加容易理解和接受。&lt;/p&gt;
&lt;p&gt;让我们回到 那如何“勾住”自定义组件中的“小写开头的类似原生标签的组件”？ 这个问题上来。&lt;/p&gt;
&lt;h5&gt;React.forwardRef() 的具体用法&lt;/h5&gt;
&lt;p&gt;React.forwardRef()包裹住要输出的组件，且将第2个参数设置为 ref 即可，示例代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React from &apos;react&apos;
    
    const ChildComponent = React.forwardRef((props,ref) =&amp;gt; {
      //子组件通过将第2个参数ref 添加到内部真正的“小写开头的类似原生标签的组件”中 
      return &amp;lt;button ref={ref}&amp;gt;{props.label}&amp;lt;/button&amp;gt;
    });
    
    /* 上面的子组件直接在父组件内定义了，如果子组件是单独的.js文件，则可以通过
       export default React.forwardRef(ChildComponent) 这种形式  */
    
    function Forward() {
      const ref = React.useRef();//父组件定义一个ref
      const clickHandle = () =&amp;gt;{
        console.log(ref.current);//父组件获得渲染后子组件中对应的DOM节点引用
      }
      return (
        &amp;lt;div&amp;gt;
            {/* 父组件通过给子组件添加属性 ref={ref} 将ref作为参数传递给子组件 */}
            &amp;lt;ChildComponent label=&apos;child bt&apos; ref={ref} /&amp;gt;
            &amp;lt;button onClick={clickHandle} &amp;gt;get child bt ref&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }
    export default Forward;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，关于useRef()基础用法、React.forwardRef()已经讲完。 这2个函数的掌握，会对下一个要讲的Hook：useImperativeHandle 非常有用。&lt;/p&gt;
</content:encoded></item><item><title>11 useMemo基础用法</title><link>https://blog.wemang.com/posts/web/react/usememo%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/usememo%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</guid><pubDate>Tue, 12 Sep 2023 09:41:43 GMT</pubDate><content:encoded>&lt;h1&gt;11 useMemo基础用法&lt;/h1&gt;
&lt;h2&gt;useMemo概念解释&lt;/h2&gt;
&lt;p&gt;我们第六个要学习的Hook(钩子函数)是useMemo，他的作用是“勾住”组件中某些处理函数的返回值，创建这些返回值对应在react原型链上的索引。当组件重新渲染时，需要再次用到这些函数返回值，此时不再重新执行一遍运算，而是直接使用之前运算过的返回值。useMemo第2个参数是处理函数的变量依赖，只有当处理函数依赖的变量发生改变时才会重新计算并保存一次函数返回结果。&lt;/p&gt;
&lt;p&gt;假设你已经对React.memo，useCallback的运行机制充分了解，那么对你而言useMemo的用法非常好理解。&lt;/p&gt;
&lt;p&gt;useCallback是将某个函数“放入到react底层原型链上，并返回该函数的索引”，而useMemo是将某个函数返回值“放入到react底层原型链上，并返回该返回值的索引”。一个是针对函数，一个是针对函数返回值。&lt;/p&gt;
&lt;p&gt;网上有些人的文章里，会提到：useCallback(fn, deps) 相当于 useMemo(() =&amp;gt; fn, deps)。&lt;/p&gt;
&lt;p&gt;这句话似乎是没有问题，但是他隐藏或者说忽略了几个重要关键点：&lt;br /&gt;
1、不是所有fn(函数)都适用的，必须是该函数有返回值，即函数有 return xx 才可以。&lt;br /&gt;
2、虽然都是fn，但是函数体内代码内容却相差很大，useCallback中的fn主要用来处理各种操作事务的代码，例如修改某变量值或加载数据等。而useMemo中的fn主要用来处理各种计算事务的代码。&lt;br /&gt;
3、useCallback和useMemo都是为了提升组件性能，但是他们两个的适用场景却不相同，不是谁是谁的替代品或谁是谁的简化版。&lt;/p&gt;
&lt;p&gt;再次强调一遍，useCallback中的函数是侧重“操作事务”，useMemo中的函数是侧重“计算结果”，永远不要在useMemo的函数中添加修改数据之类的代码。&lt;/p&gt;
&lt;p&gt;让我们回到useMemo基础学习中。&lt;/p&gt;
&lt;h2&gt;useMemo是来解决什么问题的？&lt;/h2&gt;
&lt;p&gt;答：useMemo的目的是“减少组件重新渲染时不必要的函数计算”。&lt;br /&gt;
useMemo可以将某些函数的计算结果(返回值)挂载到react底层原型链上，并返回该函数返回值的索引。当组件重新渲染时，如果useMemo依赖的数据变量未发生变化，那么直接使用原型链上保存的该函数计算结果，跳过本次无意义的重新计算，达到提高组件性能的目的。&lt;/p&gt;
&lt;p&gt;补充说明：&lt;br /&gt;
1、useMemo并不需要子组件必须使用React.memo。&lt;br /&gt;
2、“不必要的函数计算”中的函数计算必须是有一定复杂度的，例如需要1000个for循环才能计算出的某个值。如果计算量本身很简单，例如1+2，那完全没有必要使用useMemo，就直接每次重新计算一遍也无所谓。&lt;/p&gt;
&lt;h2&gt;useMemo函数源码：&lt;/h2&gt;
&lt;p&gt;回到useMemo的学习中，首先看一下React源码中的&lt;a href=&quot;https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js&quot;&gt;ReactHooks.js&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //备注：源码采用TypeScript编写，如果不懂TS代码，阅读起来稍显困难
    export function useMemo&amp;lt;T&amp;gt;(
      create: () =&amp;gt; T,
      deps: Array&amp;lt;mixed&amp;gt; | void | null,
    ): T {
      const dispatcher = resolveDispatcher();
      return dispatcher.useMemo(create, deps);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码看不懂没关系，本系列教程只是讲述“如何使用Hook”，并不是“Hook源码分析”。^_^&lt;/p&gt;
&lt;h2&gt;useMemo基本用法&lt;/h2&gt;
&lt;p&gt;useMemo(create,deps)函数通常传入2个参数，第1个参数为我们定义的一个“包含复杂计算且有返回值的函数”，第2个参数为该处理函数中存在的依赖变量，请注意凡是处理函数中有的数据变量都需要放入deps中。如果处理函数没有任何依赖变量，可以传入一个空数组[]。&lt;/p&gt;
&lt;p&gt;请注意：&lt;br /&gt;
1、useMemo只是理论上帮你进行组件计算性能优化，但是react并不能保证100%都是按照你的预期来执行的。比如说当你的网页处于离屏(休眠、挂起)等状态时，react底层原型链也许就会释放(删除)之前保存的函数返回值。等到下次网页重新被唤醒时，重新计算一次。&lt;br /&gt;
2、关于useMemo第2个参数，和useCallback一样，也许在未来版本中react会智能识别，不需要要我们再手工传入。&lt;/p&gt;
&lt;h5&gt;代码形式：&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;    const xxxValue = useMemo(() =&amp;gt; {
        let result = xxxxx;
        //经过复杂的计算后
        return result;
    }, [xx]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;拆解说明：&lt;/h5&gt;
&lt;p&gt;1、使用useMemo()将计算函数包裹住，将计算函数中使用到的数据变量作为作为第2个参数。&lt;br /&gt;
2、计算函数体内，把计算结果以 return 形式返回出去。&lt;br /&gt;
3、xxxValue 为该函数返回值在react原型链上的引用。&lt;/p&gt;
&lt;h2&gt;useMemo使用示例：&lt;/h2&gt;
&lt;p&gt;举例：若某React组件内部有2个number类型的变量num，random，有2个button，点击之后分别可以修改num，random的值。
与此同时，该组件中还要求显示出num范围内的所有质数个数总和。&lt;/p&gt;
&lt;p&gt;补充说明：加入random纯粹是为了引发组件重新渲染，方便我们查看到useMemo是否启了作用。&lt;/p&gt;
&lt;p&gt;需求分析：&lt;br /&gt;
1、显示出num范围内的所有质数个数总和，这个就是本组件中的“复杂的计算”。&lt;br /&gt;
2、只要num的值未发生变化，质数总数是固定的，那么我们应该避免每次重新渲染时都需要计算一遍。&lt;br /&gt;
3、useMemo函数，就是帮我们解决这个问题。&lt;/p&gt;
&lt;p&gt;使用useMemo，代码示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{useState,useMemo} from &apos;react&apos;

    function UseMemo() {
      const [num,setNum] = useState(2020);
      const [random,setRandom] = useState(0);

      //通过useMemo将函数内的计算结果(返回值)保存到react底层原型链上
      //totalPrimes为react底层原型链上该函数计算结果的引用
      const totalPrimes = useMemo(() =&amp;gt; {
        console.log(&apos;begin....&apos;); //这里添加一个console.log，方便验证在重新渲染时是否重新执行了一遍计算

        let total = 0; //声明质数总和对应的变量

        //以下为计算num范围内所有质数个数总和的计算代码，不需要认真阅读，只需要知道这是一段“比较复杂的计算代码”即可
        for(let i = 1; i&amp;lt;=num; i++){
            let boo = true;
            for(let j = 2; j&amp;lt;i; j++){
                if(i % j === 0){
                    boo = false;
                    break;
                }
            }
            if(boo &amp;amp;&amp;amp; i!==1){
                total ++;
            }
        }
        //复杂的计算代码到此结束

        return total;//将质数总和作为返回值return出去
      }, [num]);

      const clickHandler01 = () =&amp;gt; {
        setNum(num+1);
      }

      const clickHandler02 = () =&amp;gt; {
        setRandom(Math.floor(Math.random()*100)); //修改random的值导致整个组件重新渲染
      }

      return (
        &amp;lt;div&amp;gt;
            {num} - {totalPrimes} - {random}
            &amp;lt;button onClick={clickHandler01}&amp;gt;num + 1&amp;lt;/button&amp;gt;
            &amp;lt;button onClick={clickHandler02}&amp;gt;random&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }

    export default UseMemo;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际运行就会发现：&lt;br /&gt;
1、点击修改random的值会引发组件重新渲染，但是&lt;code&gt;{totalPrimes}&lt;/code&gt;对应的计算函数却不需要重新计算一遍。&lt;br /&gt;
2、点击修改num的值，&lt;code&gt;{totalPrimes}&lt;/code&gt;对应的计算函数肯定会重新执行一遍，因为num是该计算函数的依赖。&lt;/p&gt;
&lt;p&gt;通过这个案例，相信你对useMemo的机制和用法一定有所掌握。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useMemo基础用法已经讲完，没有高级用法，直接进入下一个Hook。&lt;/p&gt;
</content:encoded></item><item><title>10 useCallback基础用法</title><link>https://blog.wemang.com/posts/web/react/usecallback%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/usecallback%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</guid><pubDate>Mon, 11 Sep 2023 15:22:43 GMT</pubDate><content:encoded>&lt;h1&gt;10 useCallback基础用法&lt;/h1&gt;
&lt;h2&gt;useCallback概念解释&lt;/h2&gt;
&lt;p&gt;我们第五个要学习的Hook(钩子函数)是useCallback，他的作用是“勾住”组件属性中某些处理函数，创建这些函数对应在react原型链上的变量引用。useCallback第2个参数是处理函数中的依赖变量，只有当依赖变量发生改变时才会重新修改并创建新的一份处理函数。&lt;/p&gt;
&lt;h5&gt;react原型链？&lt;/h5&gt;
&lt;p&gt;我对react原型链也不太懂，你可以简单得把 react原型链 理解成 “react定义的一块内存”。我们使用某些 hook 定义的“变量或函数”都存放在这块内存里。这块内存里保存的变量或函数，并不会因为组件重新渲染而消失。&lt;br /&gt;
1、当我们需要使用时可以“对象的引用名”从该内存里获取，例如useContext&lt;br /&gt;
2、当希望更改某些变量时，可以通过特定的函数来修改该内存中变量的值，例如useState中的setXxxx()&lt;br /&gt;
3、当某些函数依赖变量发生改变时，react可以重新生成、并修改该内存中对应的函数，例如useReducer、useCallback&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;此处更新与2020年10月13日&lt;br /&gt;
今天学习了一下 JS 原型链：每一个对象或者说由 function 创建的对象，他们都有一个属性 &lt;code&gt;__proto__&lt;/code&gt;，该属性值为创建该对象的构造函数的原型对象，又称 隐式原型，而这一层的隐式原型也有 &lt;code&gt;__proto__&lt;/code&gt; 属性，即 &lt;code&gt;__proto__.__proto__&lt;/code&gt; 属性值为 Object.prototype，还可以继续再往下深入 &lt;code&gt;__proto__.__proto__.__proto__&lt;/code&gt;为了避免死循环，最终到此，即 &lt;code&gt;Object.prototype.__proto__&lt;/code&gt; 为 null。作为构造函数对象，有属性 prototype，属性值为该函数的显示原型对象。constructor 则表示原型对象的构造函数本身。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const arr = [1, 2, 3]
console.log(arr.__proto__ === Array.prototype) // true
console.log(arr.__proto__.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true

function MyFun() { this.name = &apos;puxiao&apos; }
const myFun = new MyFun()
console.log(myFun.__proto__ === MyFun.prototype) // true
console.log(MyFun.__proto__ === Function.prototype) // true
console.log(myFun.__proto__.__proto__ === Object.prototype) // true
console.log(myFun.__proto__.__proto__.__proto__) // null
console.log(Object.prototype.__proto__) // null
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;要想更加容易理解上面代码，就需要明白，所谓 object 是指 { }，而 Object 其实是 JS 内置的对象函数。同理 所谓 array 是指 []，而 Array 其实是 JS 内置的 数组函数&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;正是 JS 中 &lt;code&gt;__prototype__(隐式原型)&lt;/code&gt; &lt;code&gt;prototype(显式原型)&lt;/code&gt; &lt;code&gt;constructor(原型对象所在的构造函数本身)&lt;/code&gt; 这 3个概念，最终组合成了 庞大的 JS 功能。我们平时定义的任何 类、对象、函数 都出在这种 链条 中，以及对 这个链条中某个环节属性功能的扩展，这种组织形式就叫 JS 原型链。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;JS 原型还有一个原则就是可以无限得去扩展自身属性，当前级别的原型扩展属性之后，下层级别的对象自动就拥有该属性。&lt;/p&gt;
&lt;p&gt;那么我们可以大概推理出来，React就是巧妙利用了这种 JS 原型链的原则，将底层模块需要用到的处理函数提升到更高(或者说更加原始)的级别中。这样即使底层中发生了变化，但是他依然拥有高层中定义好的函数引用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;请重点留意“修改”这个词，因为“修改”牵扯到react最为隐秘却极其重要的一层概念。&lt;br /&gt;
“修改”有3种情况：&lt;br /&gt;
1、用完全不一样的新值去替换之前的旧值 ——&amp;gt; 这会触发react重新渲染 ——&amp;gt; 例如&lt;code&gt;{age:34}&lt;/code&gt;去替换&lt;code&gt;{age:18}&lt;/code&gt;
2、用和旧值看似一模一样的新值去替换之前的旧值 ——&amp;gt; 这依然会触发react重新渲染，因为react底层对新旧值做对比时使用的是 Object.is判断，字面上看似一模一样没有用，react依然会认为这是2个对象，依然会触发react重新渲染 ——&amp;gt; 例如&lt;code&gt;{age:18}&lt;/code&gt;去替换&lt;code&gt;{age:18}  &lt;/code&gt;
3、用旧值的引用去替换旧值 ——&amp;gt; 这次就不会触发重新渲染 ——&amp;gt; 例如&lt;code&gt;let obj={age:18}; let obj2=obj&lt;/code&gt;，用obj2去替换obj&lt;/p&gt;
&lt;p&gt;为了提高react性能，就需要用旧值的引用去替换旧值，从而阻止本次无谓的渲染。&lt;/p&gt;
&lt;p&gt;问题的关键就在于“如何获取旧值的引用”？&lt;br /&gt;
答：对于函数来说可以使用useCallback。&lt;/p&gt;
&lt;p&gt;在本章或以后的章节中，我依然会使用 react原型链 这个词，你都按照我刚才说的“react定义的一块内存”概念去理解就好了。&lt;/p&gt;
&lt;p&gt;懵圈了没？我已经尽量总结得让你容易理解了，如果你似懂非懂没有关系，本文后面会通过示例代码会让你明白如何使用。&lt;/p&gt;
&lt;p&gt;让我们先忘掉useCallback，先来学习一下以下2个知识点。&lt;/p&gt;
&lt;h5&gt;第1个知识点：React.memo() 的用法&lt;/h5&gt;
&lt;p&gt;首先我们知道，默认情况下如果父组件重新渲染，那么该父组件下的所有子组件都会随着父级的重新渲染而重新渲染。&lt;br /&gt;
1、无论子组件是类组件或是函数组件。&lt;br /&gt;
2、无论子组件在本次渲染过程中，子组件是否有任何相关的数据变化。&lt;/p&gt;
&lt;p&gt;举例，假设某父组件中有3个子组件：子组件A、子组件B、子组件C。若因为子组件A发生了某些操作，引发父组件重新渲染，这时即使子组件B和子组件C没有任何需要更改的地方，但是默认他们两个也会重新被渲染一次。&lt;/p&gt;
&lt;p&gt;为了减少这个不必要的重新渲染，如果是类组件，可以在组件shouldComponentUpdate(准备要开始更新前)生命周期函数中，通过比较props和state中前后两次的值，如果完全相等则跳过本次渲染，改为直接使用上一次渲染结果，以此提高性能提升。&lt;/p&gt;
&lt;p&gt;伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    shouldComponentUpdate(nextProps,nextStates){
      //判断xxx值是否相同，如果相同则不进行重新渲染
      return (nextProps.xxx !== this.props.xxx); //注意是 !== 而不是 !=
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了简化我们这一步操作，可以将类组件由默认继承自React.Component改为React.PureComponent。React.PureComponent默认会帮我们完成上面的浅层对比，以跳过本次重新渲染。&lt;/p&gt;
&lt;p&gt;请注意：React.PureComponent会对props上所有可枚举属性做一遍浅层对比。而不像 shouldComponentUpdate中可以有针对性的只对某属性做对比。&lt;/p&gt;
&lt;p&gt;上面讲的都是类组件，与之对应的是React.memo()，这个是针对函数组件的，作用和React.PureComponent完全相同。&lt;/p&gt;
&lt;p&gt;React.memo()的使用方法很简单，就是把要导出的函数组件包裹在React.memo中即可。&lt;/p&gt;
&lt;p&gt;伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React from &apos;react&apos;
    function Xxxx() {
      return &amp;lt;div&amp;gt;xx&amp;lt;/div&amp;gt;;
    }
    export default React.memo(Xxxx); //使用React.memo包裹住要导出的函数组件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请记住以下2点：&lt;br /&gt;
1、React.memo()只会帮我们做浅层对比，例如&lt;code&gt;props.name=&apos;puxiao&apos;&lt;/code&gt;或&lt;code&gt;props.list=[1,2,3]&lt;/code&gt;，如果是props中包含复杂的数据结构，例如&lt;code&gt;props.obj.list=[{age:34}]&lt;/code&gt;，那么有可能达不到你的预期，因为不会做到深层次对比。&lt;br /&gt;
2、使用React.memo仅仅是让该函数组件具备了可以跳过本次渲染的基础，若组件在使用的时候属性值中有某些处理函数，那么还需要配合useCallback才可以做到跳过本次重新渲染。&lt;/p&gt;
&lt;p&gt;呵，话题又回到useCallback上面了。&lt;/p&gt;
&lt;h5&gt;第2个知识点：=== 等比运算&lt;/h5&gt;
&lt;p&gt;在原生JS中，你认为&lt;br /&gt;
1、&lt;code&gt;{}==={}&lt;/code&gt; 为true还是false？&lt;br /&gt;
2、&lt;code&gt;{a:2}==={a:2}&lt;/code&gt; 为true还是false？&lt;/p&gt;
&lt;p&gt;这是一道很简单却很容易迷惑人的题目，若你对原生JS中 === 等比运算不够深入了解，你很容易会认为结果是true。&lt;/p&gt;
&lt;p&gt;如果你轻松回答出来：以上均为false，那么恭喜你是个明白人。&lt;br /&gt;
如果你疑惑了一下或者你的答案是true，那么你可以自己去JS里测试一下看结果是什么。&lt;/p&gt;
&lt;p&gt;答案是2者均是false。&lt;/p&gt;
&lt;p&gt;以{}==={}为例，虽然从字面上 === 左右两侧完全相同的，但是实际上在JS中 左右两侧分别为独立的{}对象，各自占有各自的内存空间，因此他们对比的结果是false。&lt;/p&gt;
&lt;p&gt;相反，看下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    let obj = {};
    let obj2 = obj;
    obj2.name=&apos;react&apos;;
    console.log(obj===obj2); //true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面输出结果为true，为何obj===obj2为true？  因为 obj和obj2都是对同一个对象的引用，所以对比结果为true，因为他们最终指向同一个对象。&lt;/p&gt;
&lt;p&gt;还记得本文开头对于useCallback概念解释中的那段文字吗？useCallback的作用是“勾住”组件属性中某些处理函数，创建这些函数对应在react原型链上的变量引用。&lt;/p&gt;
&lt;p&gt;呵，话题又回到useCallback上面了。&lt;/p&gt;
&lt;p&gt;划重点：记住“useCallback”和“原型链上处理函数的引用”这两个关键词，基本上你就对useCallback的原理理解一大半了。&lt;/p&gt;
&lt;p&gt;让我们回到useCallback基础学习中。&lt;/p&gt;
&lt;h2&gt;useCallback是来解决什么问题的？&lt;/h2&gt;
&lt;p&gt;答：useCallback是通过获取函数在react原型链上的引用，当即将重新渲染时，用旧值的引用去替换旧值，配合React.memo，达到“阻止组件不必要的重新渲染”。&lt;/p&gt;
&lt;p&gt;详细解释：&lt;br /&gt;
useCallback可以将组件的某些处理函数挂载到react底层原型链上，并返回该处理函数的引用，当组件每次即将要重新渲染时，确保props中该处理函数为同一函数(因为是同一对象引用，所以===运算结果一定为true)，跳过本次无意义的重新渲染，达到提高组件性能的目的。当然前提是该组件在导出时使用了React.memo()。&lt;/p&gt;
&lt;p&gt;补充说明：&lt;br /&gt;
假设某组件使用到了myfun这个处理函数，回忆一下前面提到的JS中===运算规则，考虑一下。&lt;/p&gt;
&lt;p&gt;默认不使用useCallback，其实组件执行了以下伪代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    let obj = {}; //上一次渲染时创建的props
    obj.myfun={xxx}; //props中的myfun属性值，实为独立创建的{xxx}
    
    let obj2 = {}; //本次渲染时创建的props
    obj2.myfun={xxx}; //props中的myfun属性值，实为独立创建的{xxx}
    
    if(obj.myfun === obj2.myfun){
      //跳过本次重新渲染，改为使用上一次渲染结果即可
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于obj.myfun 和 obj2.myfun 为分别独立创建的函数&lt;code&gt;{xxx}&lt;/code&gt;，所以对比结果为false，也就意味着无法跳过本次重新渲染，尽管函数&lt;code&gt;{xxx}&lt;/code&gt;字面相同。&lt;/p&gt;
&lt;p&gt;相反，如果使用useCallback，其实组件执行了以下伪代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    let myfun = {xxx}; //独立定义处理函数myfun
    
    let obj = {}; //上一次渲染时创建的props
    obj.myfun = myfun; //props中的myfun属性值，实为myfun的引用
    
    let obj2 = {}; //本次渲染时创建的props
    obj2.myfun = myfun; //props中的myfun属性值，实为myfun的引用

    if(obj.myfun === obj2.myfun){
      //跳过本次重新渲染，改为使用上一次渲染结果即可
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时 obj.myfun 和 obj2.myfun 均为myfun的引用，因此该对比结果为true，也就意味着可以顺利跳过本次渲染，达到提高组件性能的目的。&lt;/p&gt;
&lt;p&gt;以上是代码仅仅是为了示意默认子组件为什么会被迫重新渲染，以及useCallback作用机理。&lt;/p&gt;
&lt;p&gt;只有理解了这个机理，才会明白何时使用useCallback。  切记不要滥用useCallback。&lt;/p&gt;
&lt;p&gt;多说一句：你是否觉得React Hook 很绕？ 对，这就是Hook学习起来难度大的一些原因，但当你充分理解React的编程哲学思想后，用起来会如鱼得水。加油！&lt;/p&gt;
&lt;h2&gt;useCallback函数源码：&lt;/h2&gt;
&lt;p&gt;回到useCallback的学习中，首先看一下React源码中的&lt;a href=&quot;https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js&quot;&gt;ReactHooks.js&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //备注：源码采用TypeScript编写，如果不懂TS代码，阅读起来稍显困难
    export function useCallback&amp;lt;T&amp;gt;(
      callback: T,
      deps: Array&amp;lt;mixed&amp;gt; | void | null,
    ): T {
      const dispatcher = resolveDispatcher();
      return dispatcher.useCallback(callback, deps);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码看不懂没关系，本系列教程只是讲述“如何使用Hook”，并不是“Hook源码分析”。^_^&lt;br /&gt;
不过请注意第2个参数，deps为该函数依赖的数据变量，值为&lt;code&gt;Array&amp;lt;mixed&amp;gt;&lt;/code&gt; 或 void 或 null。 意味着如果该函数没有依赖的情况下，可以传入空数组[]或void或null。个人建议是传入空数组。&lt;/p&gt;
&lt;p&gt;补充一点TypeScript知识(因为我最近刚学了TypeScript)：&lt;br /&gt;
像 &lt;code&gt;&amp;lt;T\&amp;gt;(callback:T):T&lt;/code&gt; 这种类型定义称为“泛型”，里面 T 的含义为“一模一样的同类型”。&lt;br /&gt;
举例：&lt;br /&gt;
1、若T为function，即参数callback类型为function，那么函数返回值也为function。&lt;br /&gt;
2、若T为object，即参数callback类型为object，那么函数返回值也为object。&lt;/p&gt;
&lt;h2&gt;useCallback基本用法&lt;/h2&gt;
&lt;p&gt;useCallback(callback,deps)函数通常传入2个参数，第1个参数为我们定义的一个“处理函数”，通常为一个箭头函数。第2个参数为该处理函数中存在的依赖变量，请注意凡是处理函数中有的数据变量都需要放入deps中。如果处理函数没有任何依赖变量，可以传入一个空数组[]。&lt;/p&gt;
&lt;p&gt;特别强调一下：useCallback中的第2个依赖变量数组和useEffect中第2个依赖变量数组，作用完全不相同。&lt;br /&gt;
useEffect中第2个依赖变量数组是真正起作用的，是具有关键性质的。而useCallback中第2个依赖变量数组目前作用来说仅仅是起到一个辅助作用。&lt;/p&gt;
&lt;p&gt;仅仅是辅助？辅助什么了？甚至你还可能会有一个疑问，既然处理函数中所有的依赖变量都需要做为第2个参数的内容，为啥React不智能一些，让我们不传第2个参数，省略掉这一步？&lt;/p&gt;
&lt;p&gt;在React官方文档中，针对第2个参数有以下这段话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为：所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能，届时自动创建数组将成为可能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;自己体会吧，你品，你细品。&lt;/p&gt;
&lt;h5&gt;代码形式：&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;    import Button from &apos;./button&apos;; //引入我们自定义的一个组件&amp;lt;Button&amp;gt;

    //组件内部声明一个age变量
    const [age,setAge] = useState(34);

    //通过useCallback，将鼠标点击处理函数保存到React底层原型链中，并获取该函数的引用，将引用赋值给clickHandler
    const clickHandler = useCallback(() =&amp;gt; {
        setAge(age+1);
      },[age]);
    //由于该处理函数中使用到了age这个变量，因此useCallback的第2个参数中，需要将age添加进去

    //使用该处理函数，实为使用该处理函数的在React底层原型链上的引用
    return &amp;lt;Button clickHandler={clickHandler}&amp;gt;&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;拆解说明：&lt;/h5&gt;
&lt;p&gt;1、具体讲解已在上面示例代码中做了多项注释，此处不再重复；&lt;/p&gt;
&lt;h4&gt;&apos;age&apos;补充说明&lt;/h4&gt;
&lt;p&gt;1、上述代码示例中，age为该组件通过useState创建的内部变量，事实上也可以是父组件通过属性传值的props.xx中的变量。&lt;br /&gt;
2、只要依赖变量不发生变化，那么重新渲染时就可以一直使用之前创建的那个函数，达到阻止本次渲染，提升性能的目的。但是如果依赖变量发生变化，那么下次重新渲染时根据变量重新创建一份处理函数并替换React底层原型链上原有的处理函数。&lt;/p&gt;
&lt;h4&gt;&apos;clickHandler&apos;补充说明&lt;/h4&gt;
&lt;p&gt;再次强调，clickHandler实际上是真正的处理函数在React底层原型链上的引用。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt;补充说明&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;Button\&amp;gt;&lt;/code&gt;为我们自定义的一个组件，在上述代码中相当于“子组件”。&lt;/p&gt;
&lt;p&gt;上面的示例伪代码仅仅是为了演示useCallback的使用方法，实际组件运用中，如果父组件中只有1个子组件，那其实完全没有必要使用useCallback。只有父组件同时有多个子组件时，才有必要去做性能优化，防止某一个子组件引发的重新渲染也导致其他子组件跟着重新渲染。&lt;/p&gt;
&lt;h2&gt;useCallback使用示例：&lt;/h2&gt;
&lt;p&gt;若我们有一个自定组件&lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt;，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React from &apos;react&apos;
    function Button({label,clickHandler}) {
        //为了方便我们查看该子组件是否被重新渲染，这里增加一行console.log代码
        console.log(`rendering ... ${label}`);
        return &amp;lt;button onClick={clickHandler}&amp;gt;{label}&amp;lt;/button&amp;gt;;
    }
    export default React.memo(Button); //使用React.memo()包裹住要导出的组件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们要实现一个组件，功能如下：&lt;br /&gt;
1、组件内部有2个变量age，salary&lt;br /&gt;
2、有2个自定义组件Button，点击之后分别可以修改age，salary值&lt;/p&gt;
&lt;p&gt;若我们不使用useCallback，代码示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{useState,useCallback,useEffect} from &apos;react&apos;;
    import Button from &apos;./button&apos;;

    function Mybutton() {
      const [age,setAge] = useState(34);
      const [salary,setSalary] = useState(7000);

      useEffect(() =&amp;gt; {
        document.title = `Hooks - ${Math.floor(Math.random()*100)}`;
      });

      const clickHandler01 = () =&amp;gt; {
        setAge(age+1);
      };

      const clickHandler02 = () =&amp;gt; {
        setSalary(salary+1);
      };

      return (
        &amp;lt;div&amp;gt;
            {age} - {salary}
            &amp;lt;Button label=&apos;Bt01&apos; clickHandler={clickHandler01}&amp;gt;&amp;lt;/Button&amp;gt;
            &amp;lt;Button label=&apos;Bt02&apos; clickHandler={clickHandler02}&amp;gt;&amp;lt;/Button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际运行中你会发现，无论点击哪个按钮，都会收到：&lt;br /&gt;
rendering ... Bt01&lt;br /&gt;
rendering ... Bt02&lt;/p&gt;
&lt;p&gt;你只是点击操作了其中一个按钮，另外一个按钮也要跟着重新渲染一次，试想一下如果该组件中有100个子组件都要跟着重新渲染，那真的是性能浪费。&lt;/p&gt;
&lt;p&gt;我们再看一下如果使用useCallback，代码示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{useState,useCallback,useEffect} from &apos;react&apos;;
    import Button from &apos;./button&apos;;

    function Mybutton() {
      const [age,setAge] = useState(34);
      const [salary,setSalary] = useState(7000);

      useEffect(() =&amp;gt; {
        document.title = `Hooks - ${Math.floor(Math.random()*100)}`;
      });

      //使用useCallback()包裹住原来的处理函数
      const clickHandler01 = useCallback(() =&amp;gt; {
        setAge(age+1);
      },[age]);

      //使用useCallback()包裹住原来的处理函数
      const clickHandler02 = useCallback(() =&amp;gt; {
        setSalary(salary+1);
      },[salary]);

      return (
        &amp;lt;div&amp;gt;
            {age} - {salary}
            &amp;lt;Button label=&apos;Bt01&apos; clickHandler={clickHandler01}&amp;gt;&amp;lt;/Button&amp;gt;
            &amp;lt;Button label=&apos;Bt02&apos; clickHandler={clickHandler02}&amp;gt;&amp;lt;/Button&amp;gt;
        &amp;lt;/div&amp;gt;
      )
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后的代码，实际运行就会发现，当点击某个按钮时，仅仅是当前按钮重新做了一次渲染，另外一个按钮则没有重新渲染，而是直接使用上一次渲染结果。&lt;/p&gt;
&lt;p&gt;使用useCallback减少子组件没有必要的渲染目的达成。&lt;/p&gt;
&lt;p&gt;useCallback用法很简单，就是包裹住原本的处理函数。关键点在于你要理解useCallback背后的机理，才能知道在什么情况下可以使用useCallback。否则很容易滥用 useCallback，反而造成性能的浪费。&lt;/p&gt;
&lt;h2&gt;思考题&lt;/h2&gt;
&lt;p&gt;假设上面示例代码中，做以下修改：每个按钮上新增一个属性：random={Math.floor(Math.random()*100)}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;Button label=&apos;Bt01&apos; clickHandler={clickHandler01}&amp;gt;&amp;lt;/Button&amp;gt;
    &amp;lt;Button label=&apos;Bt02&apos; clickHandler={clickHandler02}&amp;gt;&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;修改为
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;Button label=&apos;Bt01&apos; clickHandler={clickHandler01} random={Math.floor(Math.random()*100)}&amp;gt;&amp;lt;/Button&amp;gt;
    &amp;lt;Button label=&apos;Bt02&apos; clickHandler={clickHandler02} random={Math.floor(Math.random()*100)}&amp;gt;&amp;lt;/Button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么请问，此时我们针对性能优化而使用的useCallback还有意义吗？&lt;/p&gt;
&lt;p&gt;答：没有任何意义，虽然我们使用useCallback保证了每次clickHandler是相同的，可是 random 的值每次却是随机不一样的，尽管子组件&lt;code&gt;&amp;lt;Button&amp;gt;&lt;/code&gt;并没有使用到 random 这个值，但是它的加入造成了 props 每次都不一样(其实是 props.random 不一样)，结果就是子组件每一次都会被重新渲染。所以此时useCallback已经失去了存在的意义。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useCallback基础用法已经讲完，没有高级用法，直接进入下一个Hook。&lt;/p&gt;
</content:encoded></item><item><title>09 useReducer高级用法</title><link>https://blog.wemang.com/posts/web/react/usereducer%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/usereducer%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95/</guid><pubDate>Thu, 07 Sep 2023 17:37:05 GMT</pubDate><content:encoded>&lt;h1&gt;09 useReducer高级用法&lt;/h1&gt;
&lt;p&gt;所谓高级用法，只不过是一些深层知识点和实用技巧，你甚至可以把本章当做对前面知识点的一个巩固和学习。&lt;/p&gt;
&lt;h2&gt;使用useReducer来管理复杂类型的数据&lt;/h2&gt;
&lt;p&gt;举例，若某组件内通过ajax请求数据，获取最新一条站内短信文字，需要组件显示整个ajax过程及结果：&lt;br /&gt;
1、当ajax开始请求时，界面显示“loading...”；&lt;br /&gt;
2、当ajax请求发生错误时，界面显示“wrong!”;&lt;br /&gt;
3、当ajax请求成功获取数据时，界面显示获取到的数据内容；&lt;/p&gt;
&lt;p&gt;如果我们使用useState来实现上述功能，伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    function Component() {
      const [loading,setLoading] = useState(true); //是否ajax请求中，默认为true
      const [result,setResult] = useState(&apos;&apos;); //请求数据内容，默认为&apos;&apos;
      const [error,setError] = useState(false); //请求是否发生错误，默认为false
    
      {
          //ajax请求成功
          setLoading(false);
          setResult(&apos;You have a good news!&apos;);//请注意，这是一行伪代码，只是为了演示，并不是真正ajax获取的结果
          setError(false);
    
          //ajax请求错误
          setLoading(false);
          setError(true);
      }
    
      return &amp;lt;div&amp;gt;
        {loading ? &apos;loading...&apos; : result}
        {error ? &apos;wrong!&apos; : null}
      &amp;lt;/div&amp;gt;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们使用useReducer来实现，则可将上述3个变量都放在我们定义的变量state中，伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    const initralData = {loading: true,result: &apos;&apos;,error: false};
    
    const reducer = (state, action) =&amp;gt; {
      switch (action.type) {
        case &apos;succes&apos;:
            return {loading:false,result:action.res,error:false}
        case &apos;error&apos;:
            return {loading:false,error:true}
      }
    }
    
    function Component() {
      const [state, dispatch] = useReducer(reducer, initralData);
    
      {
          //ajax请求成功
          dispatch({type:&apos;succes&apos;,res:&apos;You have a good news!&apos;});
    
          //ajax请求错误
          dispatch({type:&apos;error&apos;});
      }
    
      return &amp;lt;div&amp;gt;
        {state.loading ? &apos;loading...&apos; : state.result}
        {state.error ? &apos;wrong!&apos; : null}
      &amp;lt;/div&amp;gt;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可能会有疑问？&lt;br /&gt;
1、为什么看上去使用useReducer后代码变得更多？
答：因为使用useReducer，我们将修改数据拆分为2个部分，即“抛出修改事件和事件修改处理函数”。虽然代码增多了，但是逻辑更加清晰。&lt;/p&gt;
&lt;p&gt;2、为什么不使用useState，同时把它对应的变量也做成一个obj，就像useReducer的initralData那种？&lt;br /&gt;
答：单纯从1次ajax请求很难看出使用useState或useReducer的差异，但是试想一下多次且ajax返回值在结构类型上容易发生变更，那么使用useReducer这种更加利于代码阅读、功能扩展。&lt;/p&gt;
&lt;h2&gt;使用useContext和useReducer实现操作全局共享数据&lt;/h2&gt;
&lt;p&gt;试想一下，如果想实现以下组件需求：&lt;br /&gt;
1、父组件中定义某变量xx；&lt;br /&gt;
2、任何层级下的子组件都可以轻松获取变量xx、并且可以“修改”变量xx；&lt;/p&gt;
&lt;p&gt;注意这里的修改是加引号的，因为事实上你永远无法以直接赋值的方式进行修改，永远都需要调用父级组件提供的方法来修改。&lt;/p&gt;
&lt;h4&gt;需求分析&lt;/h4&gt;
&lt;p&gt;首先这个功能是类组件无法做到的，也是React 16.8版本以前根本不能实现的，今天，当你使用Hook可轻松实现类似 Redux 共享数据管理功能。&lt;/p&gt;
&lt;h4&gt;实现原理&lt;/h4&gt;
&lt;p&gt;用 useContext 实现“获取全局数据”&lt;br /&gt;
用 useReducer 实现“修改全局数据”&lt;/p&gt;
&lt;h4&gt;实现思路&lt;/h4&gt;
&lt;p&gt;1、用React.createContext()定义一个全局数据对象；&lt;br /&gt;
2、在父组件中用 useReducer 定义全局变量xx和负责抛出修改事件的dispatch；&lt;br /&gt;
3、在父组件之外，定义负责具体修改全局变量的处理函数reducer，根据修改xx事件类型和参数，执行修改xx的值；&lt;br /&gt;
4、在父组件中用 &lt;code&gt;&amp;lt;XxxContext.Provider value={{xx,dispatch}}&amp;gt;&lt;/code&gt; 标签把 全局共享数据和负责抛出修改xx的dispatch 暴露给子组件；&lt;br /&gt;
5、在子组件中用 useContext 获取全局变量；&lt;br /&gt;
6、在子组件中用 xxContext.dispatch 去抛出修改xx的事件，携带修改事件类型和参数；&lt;/p&gt;
&lt;h4&gt;补充说明&lt;/h4&gt;
&lt;p&gt;上面一直提到了 “抛出事件” “事件处理函数” &quot;dispatch&quot; 都是字面上的，不是真正意义上的事件驱动。  这些都只是 React 暴露给我们的函数或形参。 真正的事件驱动是由 React Hook 底层为我们完成的。&lt;/p&gt;
&lt;p&gt;以上观点仅为个人理解，不能保证100%正确。&lt;/p&gt;
&lt;h4&gt;伪代码演示&lt;/h4&gt;
&lt;p&gt;假设React组件需求为：&lt;br /&gt;
1、有全局数据变量count；&lt;br /&gt;
2、不同层级的子组件均可获取并修改全局变量count；&lt;/p&gt;
&lt;p&gt;共享对象 代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React from &apos;react&apos;;
    const CountContext = React.createContext();
    export default CountContext;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;父组件 代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React, { useReducer } from &apos;react&apos;;
    import CountContext from &apos;./CountContext&apos;;
    import ComponentA from &apos;./ComponentA&apos;;
    import ComponentB from &apos;./ComponentB&apos;;
    import ComponentC from &apos;./ComponentC&apos;;
    
    const initialCount = 0; //定义count的默认值
    
    //修改count事件处理函数，根据修改参数进行处理
    function reducer(state, action) {
    //注意这里先判断事件类型，然后结合携带的参数param 来最终修改count
    switch (action.type) {
        case &apos;add&apos;:
            return state + action.param;
        case &apos;sub&apos;:
            return state - action.param;
        case &apos;mul&apos;:
            return state * action.param;
        case &apos;reset&apos;:
            return initialCount;
        default:
            console.log(&apos;what?&apos;);
            return state;
    }
    }
    
    function ParentComponent() {
      //定义全局变量count，以及负责抛出修改事件的dispatch
      const [count, dispatch] = useReducer(reducer, initialCount);
    
      //请注意：value={{count,dispatch} 是整个代码的核心，把将count、dispatch暴露给所有子组件
      return &amp;lt;CountContext.Provider value={{count,dispatch}}&amp;gt;
        &amp;lt;div&amp;gt;
            ParentComponent - count={count}
            &amp;lt;ComponentA /&amp;gt;
            &amp;lt;ComponentB /&amp;gt;
            &amp;lt;ComponentC /&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/CountContext.Provider&amp;gt;
    }
    
    export default ParentComponent;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;子组件A 代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{ useState, useContext } from &apos;react&apos;;
    import CountContext from &apos;./CountContext&apos;;
    
    function CopmpoentA() {
      const [param,setParam] = useState(1);
      //引入全局共享对象，获取全局变量count，以及修改count对应的dispatch
      const countContext = useContext(CountContext);
    
      const inputChangeHandler = (eve) =&amp;gt; {
        setParam(eve.target.value);
      }
    
      const doHandler = () =&amp;gt; {
        //若想修改全局count，先获取count对应的修改抛出事件对象dispatch，然后通过dispatch将修改内容抛出
        //抛出的修改内容为：{type:&apos;add&apos;,param:xxx}，即告诉count的修改事件处理函数，本次修改的类型为add，参数是param
        //这里的add和param完全是根据自己实际需求自己定义的
        countContext.dispatch({type:&apos;add&apos;,param:Number(param)});
      }
    
      const resetHandler = () =&amp;gt; {
        countContext.dispatch({type:&apos;reset&apos;});
      }
    
      return &amp;lt;div&amp;gt;
            ComponentA - count={countContext.count}
            &amp;lt;input type=&apos;number&apos; value={param} onChange={inputChangeHandler} /&amp;gt;
            &amp;lt;button onClick={doHandler}&amp;gt;add {param}&amp;lt;/button&amp;gt;
            &amp;lt;button onClick={resetHandler}&amp;gt;reset&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    }
    
    export default CopmpoentA;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：&lt;br /&gt;
1、3个子组件他们主要区别是组件内 doHandler 函数，对count进行不同形式的修改；&lt;br /&gt;
2、3个子组件分别可以实现对全局变量 count 的获取与修改；&lt;br /&gt;
3、当任何一个子组件对count进行了修改，都会立即反映在其他子组件中，实现子组件之间的数据共享。&lt;/p&gt;
&lt;p&gt;至此，实现了比较简单的，类似 Redux 全局数据管理效果。&lt;/p&gt;
&lt;h2&gt;为什么不使用Redux？&lt;/h2&gt;
&lt;p&gt;这个问题以前提出过，现在可以明确回答：因为我自己使用 useReducer + useContext 自己可以轻松实现，干嘛还要用Redux。&lt;br /&gt;
再见 Redux。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下内容更新于 2021.05.18&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;忘掉 Redux，忘掉 useReducer+useContext，拥抱 Recoil 吧&lt;/h2&gt;
&lt;p&gt;强烈推荐使用 React 开发人员针对 Hooks 函数组件推出的新一代状态管理库：Recoil&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://recoiljs.org&quot;&gt;Recoil 官方网站&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我写的 &lt;a href=&quot;https://github.com/puxiao/recoil-tutorial&quot;&gt;Recoil 教程&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以上内容更新于 2021.05.18&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;什么时候用useState？什么时候用useReducer？&lt;/h2&gt;
&lt;p&gt;本人的建议是：组件自己内部的简单逻辑变量用useState、多个组件之间共享的复杂逻辑变量用useReducer。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useReducer高级用法已经讲完，useReducer可以让我们实现复杂逻辑的数据修改，结合useContext更能做到全局数据共享和修改。&lt;/p&gt;
&lt;p&gt;目前已经学习过的4个Hook函数useState、useEffect、useContext、useReducer，他们都是用来实现组件某些具体业务功能的，而接下来要学习的Hook函数则是用来提高组件整体性能的，例如第5个Hook函数useCallback和第6个Hook函数useMemo。&lt;/p&gt;
</content:encoded></item><item><title>08 useReducer基础用法</title><link>https://blog.wemang.com/posts/web/react/usereducer%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/usereducer%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</guid><pubDate>Thu, 07 Sep 2023 17:36:58 GMT</pubDate><content:encoded>&lt;h1&gt;08 useReducer基础用法&lt;/h1&gt;
&lt;h2&gt;useReducer概念解释&lt;/h2&gt;
&lt;p&gt;我们第四个要学习的Hook(钩子函数)是useReducer，他的作用是“勾住”某些自定义数据对应的dispatch所引发的数据更改事件。useReducer可以替代useState，实现更为复杂逻辑的数据修改。&lt;/p&gt;
&lt;p&gt;在React 16.8版本以前，通常需要使用第三方Redux来管理React的公共数据，但自从 React Hook 概念出现以后，可以使用 useContext + useReducer 轻松实现 Redux 相似功能。这一部分会在 “useReducer高级用法” 中做详细讲解。&lt;/p&gt;
&lt;p&gt;让我们回到useReducer基础学习中。&lt;/p&gt;
&lt;h2&gt;useReducer是来解决什么问题的？&lt;/h2&gt;
&lt;p&gt;答：useReducer是useState的升级版(实际上应该是原始版)，可以实现复杂逻辑修改，而不是像useState那样只是直接赋值修改。&lt;/p&gt;
&lt;p&gt;补充说明：&lt;br /&gt;
1、在React源码中，实际上useState就是由useReducer实现的，所以useReducer准确来说是useState的原始版。&lt;br /&gt;
2、无论哪一个Hook函数，本质上都是通过事件驱动来实现视图层更新的。&lt;/p&gt;
&lt;h2&gt;useReducer函数源码：&lt;/h2&gt;
&lt;p&gt;回到useReducer的学习中，首先看一下React源码中的&lt;a href=&quot;https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js&quot;&gt;ReactHooks.js&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //备注：源码采用TypeScript编写，如果不懂TS代码，阅读起来稍显困难
    export function useReducer&amp;lt;S, I, A&amp;gt;(
      reducer: (S, A) =&amp;gt; S,
      initialArg: I,
      init?: I =&amp;gt; S,
    ): [S, Dispatch&amp;lt;A&amp;gt;] {
      const dispatcher = resolveDispatcher();
      return dispatcher.useReducer(reducer, initialArg, init);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码看不懂没关系，本系列教程只是讲述“如何使用Hook”，并不是“Hook源码分析”。之所以贴出源码只是想让你重点看一下useReducer函数的第3个参数。一般我们只传2个参数，如果有一天你看到有人为了某些不常用的目的传了3个参数，你应该理解，第3个参数其实只是第1和第2个参数的某种转化。事实上你可以完全忽略这个问题，每次值传2个参数即可。^_^&lt;/p&gt;
&lt;h2&gt;useReducer基本用法&lt;/h2&gt;
&lt;p&gt;useReducer(reducer,initialValue)函数通常传入2个参数，第1个参数为我们定义的一个“由dispatch引发的数据修改处理函数”，第2个参数为自定义数据的默认值，useReducer函数会返回自定义变量的引用和该自定义变量对应的“dispatch”。&lt;/p&gt;
&lt;p&gt;请注意，当你看到了dispatch，肯定想到了原生JS中的EventEmitter，事实上React Hook帮我们做了底层的事件驱动处理，我们拿到的dispatch以及“事件处理函数”reducer，都时被React Hook 封装过后的，并不是真正的抛出和事件处理函数。&lt;/p&gt;
&lt;p&gt;但是为了更容易让你理解，本文依然会在讲解useReducer时使用到“事件抛出、事件处理函数”等文字。&lt;/p&gt;
&lt;p&gt;如果你了解事件驱动，使用过EventEmitter，或者你使用过Redux，那么你会很容易理解useReducer的用法。&lt;/p&gt;
&lt;h5&gt;代码形式：&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;    import React, { useReducer } from &apos;react&apos;; //引入useReducer
    
    //定义好“事件处理函数” reducer
    function reducer(state, action) {
      switch (action) {
        case &apos;xx&apos;:
            return xxxx;
        case &apos;xx&apos;:
            return xxxx;
        default:
            return xxxx;
      }
    }

    function Component(){
      //声明一个变量xxx，以及对应修改xxx的dispatch
      //将事件处理函数reducer和默认值initialValue作为参数传递给useReducer
      const [xxx, dispatch] = useReducer(reducer, initialValue); 

      //若想获取xxx的值，直接使用xxx即可
      
      //若想修改xxx的值，通过dispatch来修改
      dispatch(&apos;xx&apos;);
    }

    //请注意，上述代码中的action只是最基础的字符串形式，事实上action可以是多属性的object，这样可以自定义更多属性和更多参数值
    //例如 action 可以是 {type:&apos;xx&apos;,param:xxx}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;拆解说明：&lt;/h5&gt;
&lt;p&gt;1、具体讲解已在上面示例代码中做了多项注释，此处不再重复；&lt;/p&gt;
&lt;h4&gt;&apos;reducer&apos;补充说明&lt;/h4&gt;
&lt;p&gt;1、reducer英文单词本身意思是“减速器、还原剂”，但是本文中一直把reducer称呼为“事件处理函数”，但事实上reducer确实扮演一个事件处理函数。&lt;br /&gt;
2、千万不要把useReducer中的reducer 和 原生JS中的Array.prototype.reduce()弄混淆，他们两个只是刚好都使用了这个reduce单词而已，两者本身没有任何内在关联。&lt;/p&gt;
&lt;h4&gt;&apos;xxx&apos;补充说明&lt;/h4&gt;
&lt;p&gt;假设我们定义的变量名为xxx，那么只能通过dispatch来修改xxx，不要尝试通过 xxx = newValue 这种形式直接修改变量的值，React 不允许这样做。&lt;/p&gt;
&lt;h4&gt;&apos;dispatch&apos;补充说明&lt;/h4&gt;
&lt;p&gt;再次强调，dispacth并不是真正的Event.dispatch，但是你完全可以把它当成Event.dispatch来理解，只不过useReducer中的dispacth(xxx)函数抛出内容不是event，而是一个包含修改信息的对象，该对象不仅可以是字符串，还可以是复杂对象。&lt;/p&gt;
&lt;h4&gt;&apos;initialValue&apos;补充说明&lt;/h4&gt;
&lt;p&gt;initialValue是我们自定义变量的默认值，该值可以是简单类型(number、string)，也可以是复杂类型(object、array)。&lt;br /&gt;
推荐建议：即使该值是简单类型，也建议单独定义出来而不是直接将值写在useReducer函数中，因为单独定义可以让我们更加清晰读懂数据结构，尤其是initialValue为复杂类型时。&lt;/p&gt;
&lt;h2&gt;useReducer使用示例1：&lt;/h2&gt;
&lt;p&gt;举例：若某React组件内部有一个变量count，默认值为0，有3个button，点击之后分别可以修改count的值。3个按钮具体的功能为：第1个button点击之后count+1，第2个button点击之后count -1，第3个button点击之后 count x 2 (翻倍)。&lt;/p&gt;
&lt;p&gt;若使用useState来实现，那肯定没问题，每个button点击之后分别运算得到对应的新值，将该值直接通过setCount赋予给count。&lt;/p&gt;
&lt;p&gt;若使用useReducer来实现相同功能，代码示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React, { useReducer } from &apos;react&apos;;

    function reducer(state,action){
      switch(action){
        case &apos;add&apos;:
            return state + 1;
        case &apos;sub&apos;:
            return state - 1;
        case &apos;mul&apos;:
            return state * 2;
        default:
            console.log(&apos;what?&apos;);
            return state;
      }
    }

    function CountComponent() {
      const [count, dispatch] = useReducer(reducer,0);

      return &amp;lt;div&amp;gt;
        {count}
        &amp;lt;button onClick={() =&amp;gt; {dispatch(&apos;add&apos;)}} &amp;gt;add&amp;lt;/button&amp;gt;
        &amp;lt;button onClick={() =&amp;gt; {dispatch(&apos;sub&apos;)}} &amp;gt;sub&amp;lt;/button&amp;gt;
        &amp;lt;button onClick={() =&amp;gt; {dispatch(&apos;mul&apos;)}} &amp;gt;mul&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;;
    }

    export default CountComponent;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码分析：&lt;br /&gt;
3个按钮点击之后，不再具体去直接修改count的值，而是采用 dispatche(&apos;xxx&apos;)的形式 “抛出修改count的事件”，事件处理函数reducer“捕获到修改count的事件后”，根据该事件携带的命令类型来进一步判断，并真正执行对count的修改。&lt;/p&gt;
&lt;p&gt;请注意上面这句话中加引号的语句，本文只是以事件驱动的语言来描述整个过程，目的希望你能更加容易理解。&lt;/p&gt;
&lt;p&gt;3个按钮只是负责通知reducer“我希望做什么事情”，具体怎么做完全由reducer来执行。这样实现了修改数据具体执行逻辑与按钮点击处理函数的抽离。&lt;/p&gt;
&lt;p&gt;如果不使用useReducer，而是使用之前学习过的useState，那么对count的每一种修改逻辑代码，都必须分散写在每个按钮的点击事件处理函数中。&lt;/p&gt;
&lt;p&gt;若只是修改count的功能，那么useReducer的优势还未全部体现出来，我们接着看另外一个示例。&lt;/p&gt;
&lt;h2&gt;useReducer使用示例2&lt;/h2&gt;
&lt;p&gt;举例：在示例1中对count 执行的修改，数值变动都是固定的，即 +1、-1、x 2。假设我们希望按钮点击之后，能够自主控制增加多少、减多少、或乘以几，这个效果该怎么实现呢？&lt;/p&gt;
&lt;p&gt;很简单，我们将&lt;code&gt;dispatch(&apos;xxx&apos;)&lt;/code&gt;中的xxx由字符串改为obj，obj可以携带更多属性作为参数传给reducer。 比如之前对 &quot;加&quot;的命令 &lt;code&gt;dispatch(&apos;add&apos;)&lt;/code&gt;，修改为 &lt;code&gt;dispatch({type:&apos;add&apos;,param:2})&lt;/code&gt;。 reducer可以通过action.type来区分是哪种命令、通过action.param来获取对应的参数。&lt;/p&gt;
&lt;p&gt;为了简化代码，我们将在点击按钮后，随机产生一个数字，并将该数字作为 param 的值，传递给reducer。&lt;/p&gt;
&lt;p&gt;修改后的代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React, { useReducer } from &apos;react&apos;;

    function reducer(state,action){
      //根据action.type来判断该执行哪种修改
      switch(action.type){
        case &apos;add&apos;:
          //count 最终加多少，取决于 action.param 的值
          return state + action.param;
        case &apos;sub&apos;:
          return state - action.param;
        case &apos;mul&apos;:
          return state * action.param;
        default:
          console.log(&apos;what?&apos;);
          return state;
      }
    }

    function getRandom(){
      return Math.floor(Math.random()*10);
    }

    function CountComponent() {
      const [count, dispatch] = useReducer(reducer,0);

      return &amp;lt;div&amp;gt;
        {count}
        &amp;lt;button onClick={() =&amp;gt; {dispatch({type:&apos;add&apos;,param:getRandom()})}} &amp;gt;add&amp;lt;/button&amp;gt;
        &amp;lt;button onClick={() =&amp;gt; {dispatch({type:&apos;sub&apos;,param:getRandom()})}} &amp;gt;sub&amp;lt;/button&amp;gt;
        &amp;lt;button onClick={() =&amp;gt; {dispatch({type:&apos;mul&apos;,param:getRandom()})}} &amp;gt;mul&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;;
    }

    export default CountComponent;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的道理，我们可以把示例中的count由简单类型改为复杂类型，来储存更多的变量。 但是，建议不要把 useReducer 对应的变量设计的过于复杂。&lt;/p&gt;
&lt;p&gt;使用useReducer，可以让我们使用比较复杂的逻辑和参数对内部变量进行修改。&lt;/p&gt;
&lt;p&gt;不过你是否发现，示例1和示例2中所有的变量都是在同一个组件内定义和修改的，现实项目中肯定牵扯到不同模块组件之间共享并修改某个变量，那又该怎么办呢？&lt;br /&gt;
在下一章节 useReducer高级用法 中，我们会详细讲述如何用 useReducer + useContext 来实现全局不同层级组件共享并修改某变量。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useReducer基础用法已经讲完。&lt;/p&gt;
</content:encoded></item><item><title>07 useContext高级用法</title><link>https://blog.wemang.com/posts/web/react/usecontext%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/usecontext%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95/</guid><pubDate>Thu, 07 Sep 2023 11:40:56 GMT</pubDate><content:encoded>&lt;h1&gt;07 useContext高级用法&lt;/h1&gt;
&lt;p&gt;所谓高级用法，只不过是一些深层知识点和实用技巧，你甚至可以把本章当做对前面知识点的一个巩固和学习。&lt;/p&gt;
&lt;h2&gt;同时传递多个共享数据值给1个子组件&lt;/h2&gt;
&lt;p&gt;实现以下组件需求：&lt;br /&gt;
1、有2个共享数据对象 UserContext、NewsContext；&lt;br /&gt;
2、父组件为AppComponent、子组件为ChildComponent；&lt;br /&gt;
3、父组件需要同时将UserContext、NewsContext的数据同时传递给子组件；&lt;/p&gt;
&lt;p&gt;实现代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React,{ useContext } from &apos;react&apos;

    const UserContext = React.createContext();
    const NewsContext = React.createContext();

    function AppComponent() {
      return (
        &amp;lt;UserContext.Provider value={{name:&apos;puxiao&apos;}}&amp;gt;
            &amp;lt;NewsContext.Provider value={{title:&apos;Hello React Hook.&apos;}}&amp;gt;
                &amp;lt;ChildComponent /&amp;gt;
            &amp;lt;/NewsContext.Provider&amp;gt;
        &amp;lt;/UserContext.Provider&amp;gt;
      )
    }

    function ChildComponent(){
      const user = useContext(UserContext);
      const news = useContext(NewsContext);
      return &amp;lt;div&amp;gt;
        {user.name} - {news.title}
      &amp;lt;/div&amp;gt;
    }

    export default AppComponent;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码分析：&lt;br /&gt;
1、父组件同时要实现传递2个共享数据对象value值，需要使用&lt;code&gt;&amp;lt;XxxContext.Provider value={obj}&amp;gt;&lt;/code&gt;标签进行2次嵌套。&lt;br /&gt;
2、子组件使用了useContext，他可以自由随意使用父组件传递过来的共享数据value，并不需要多次嵌套获取。&lt;/p&gt;
&lt;h2&gt;同时将1个共享数据值传递给多个子组件&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;&amp;lt;XxxContext.Provider&amp;gt;&amp;lt;/XxxContext.Provider&amp;gt;&lt;/code&gt;标签将多个子组件包裹起来，即可实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;XxxContext.Provider value={{name:&apos;puxiao&apos;}}&amp;gt;
        &amp;lt;ComponentA /&amp;gt;
        &amp;lt;ComponentB /&amp;gt;
        &amp;lt;ComponentC /&amp;gt;
    &amp;lt;/XxxContext.Provider&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3个子组件&lt;code&gt;&amp;lt;ComponentA /\&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;ComponentB /\&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;ComponentC /\&amp;gt;&lt;/code&gt;都可使用useContext获取共享数据值。&lt;/p&gt;
&lt;h2&gt;为什么不使用Redux？&lt;/h2&gt;
&lt;p&gt;在Hook出现以前，React主要负责视图层的渲染，并不负责组件数据状态管理，所以才有了第三方Redux模块，专门来负责React的数据管理。&lt;/p&gt;
&lt;p&gt;但是自从有了Hook后，使用React Hook 进行函数组件开发，实现数据状态管理变得切实可行。只要根据实际项目需求，使用useContext以及下一章节要学习的useReducer，一定程度上是可以满足常见需求的。&lt;/p&gt;
&lt;p&gt;毕竟使用Redux会增大项目复杂度，此外还要花费学习Redux成本。&lt;/p&gt;
&lt;p&gt;具体需求具体分析，不必过分追求Redux。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useContext高级用法已经讲完，useContext降低了组件之间数据传递的复杂性，让我们编写代码更加心情愉悦，而不用去关心层层嵌套问题。&lt;/p&gt;
&lt;p&gt;接下来学习第4个Hook函数useReducer。&lt;/p&gt;
</content:encoded></item><item><title>06 useContext基础用法</title><link>https://blog.wemang.com/posts/web/react/usecontext%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/usecontext%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</guid><pubDate>Thu, 07 Sep 2023 11:40:49 GMT</pubDate><content:encoded>&lt;h1&gt;06 useContext基础用法&lt;/h1&gt;
&lt;h2&gt;useContext概念解释&lt;/h2&gt;
&lt;p&gt;我们第三个要学习的Hook(钩子函数)是useContext，他的作用是“勾住”获取由React.createContext()创建、&lt;code&gt;&amp;lt;XxxContext.Provider&amp;gt;&lt;/code&gt;添加设置的共享数据value值。useContext可以替代&lt;code&gt;&amp;lt;XxxContext.Consumer&amp;gt;&lt;/code&gt;标签，简化获取共享数据的代码。&lt;/p&gt;
&lt;p&gt;我们知道，原本不同级别的组件之间传递属性值，必须逐层传递，即使中间层的组件不需要这些数据。&lt;br /&gt;
注意：这里说的组件指React所有组件，包含类组件和函数组件。&lt;/p&gt;
&lt;p&gt;数据层层传递增加了组件的复杂性，降低了可复用性。为了解决这个问题，我们可以使用以下方式：&lt;br /&gt;
1、在组件顶层或单独的模块中，由React.createContext()创建一个共享数据对象；&lt;br /&gt;
2、在父组件中添加共享数据对象的引用，通过且只能通过&lt;code&gt;&amp;lt;XxxContext.provider value={xx:&apos;xxx&apos;}&amp;gt;&amp;lt;/XxxContext.provider&amp;gt;&lt;/code&gt;的形式将数据传递给子组件。请注意传值必须使用&lt;code&gt;value={obj}&lt;/code&gt;这种形式，若值本身为字符串则可以改为 value=&apos;xxx&apos;；&lt;br /&gt;
3、若下一层的子组件用不到共享数据对象中的数据，则可以不做任何属性标签传递；&lt;br /&gt;
4、若某一层的子组件需要用到共享数据对象的数据，则可通过&lt;code&gt;&amp;lt;XxxContext.Consumer&amp;gt;&amp;lt;/XxxContext.Consumer&amp;gt;&lt;/code&gt;获取到数据；&lt;br /&gt;
5、在类组件中除了&lt;code&gt;&amp;lt;XxxContext.Consumer&amp;gt;&lt;/code&gt;标签，还有另外一种获取共享数据方式：static xxx = XxxContext; 但是这种形式在函数组件中无法使用。&lt;/p&gt;
&lt;p&gt;简而言之&lt;code&gt;&amp;lt;XxxContext.Provider&amp;gt;&lt;/code&gt;用来添加共享数据、&lt;code&gt;&amp;lt;XxxContext.Consumer&amp;gt;&lt;/code&gt;用来获取共享数据。&lt;br /&gt;
备注：provider单词本意为供应者、consumer单词本意为消费者，刚好对应他们相对于共享数据的关系。&lt;/p&gt;
&lt;p&gt;上面简单描述了React.createContext()的用法，由于本系列文章主要讲Hook的使用方法，React本身的知识点并不是重点讲解对象。若你对React.createContext()、&lt;code&gt;&amp;lt;XxxContext.Provider&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;XxxContext.Consumer&amp;gt;&lt;/code&gt;的用法还不太明白，请通过其他途径自行学习。&lt;/p&gt;
&lt;p&gt;让我们回到useContext学习中。&lt;/p&gt;
&lt;h2&gt;useContext是来解决什么问题的？&lt;/h2&gt;
&lt;p&gt;答：useContext是&lt;code&gt;&amp;lt;XxxContext.Consumer&amp;gt;&lt;/code&gt;的替代品，可以大量简化获取共享数据值的代码。&lt;/p&gt;
&lt;p&gt;补充说明：&lt;br /&gt;
1、函数组件和类组件，对于&lt;code&gt;&amp;lt;XxxContext.Provider&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;XxxContext.Consumer&amp;gt;&lt;/code&gt;使用方式没有任何差别。&lt;br /&gt;
2、你可以在函数组件中不使用useContext，继续使用&lt;code&gt;&amp;lt;XxxContext.Consumer&amp;gt;&lt;/code&gt;，这都没问题。只不过使用useContext后，可以让获取共享数据相关代码简单一些。&lt;/p&gt;
&lt;h2&gt;useContext函数源码&lt;/h2&gt;
&lt;p&gt;回到useContext的学习中，首先看一下React源码中的&lt;a href=&quot;https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js&quot;&gt;ReactHooks.js&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //备注：源码采用TypeScript编写，如果不懂TS代码，阅读起来稍显困难
    export function useContext&amp;lt;T&amp;gt;(
      Context: ReactContext&amp;lt;T&amp;gt;,
      unstable_observedBits: number | boolean | void,): T {
      const dispatcher = resolveDispatcher();
      if (__DEV__) {
        if (unstable_observedBits !== undefined) {
          console.error(
            &apos;useContext() second argument is reserved for future &apos; +
            &apos;use in React. Passing it is not supported. &apos; +
            &apos;You passed: %s.%s&apos;,
            unstable_observedBits,
            typeof unstable_observedBits === &apos;number&apos; &amp;amp;&amp;amp; Array.isArray(arguments[2])
            ? &apos;\n\nDid you call array.map(useContext)? &apos; +
              &apos;Calling Hooks inside a loop is not supported. &apos; +
              &apos;Learn more at https://fb.me/rules-of-hooks&apos;
            : &apos;&apos;,
          );
      }

      // TODO: add a more generic warning for invalid values.
      if ((Context: any)._context !== undefined) {
        const realContext = (Context: any)._context;
        // Don&apos;t deduplicate because this legitimately causes bugs
        // and nobody should be using this in existing code.
        if (realContext.Consumer === Context) {
          console.error(
            &apos;Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be &apos; +
              &apos;removed in a future major release. Did you mean to call useContext(Context) instead?&apos;,
          );
        } else if (realContext.Provider === Context) {
          console.error(
            &apos;Calling useContext(Context.Provider) is not supported. &apos; +
              &apos;Did you mean to call useContext(Context) instead?&apos;,
          );
        }
      }
    }
      return dispatcher.useContext(Context, unstable_observedBits);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码看不懂没关系，本系列教程只是讲述“如何使用Hook”，并不是“Hook源码分析”。之所以贴出源码只是想让你看懂以后告诉我，反正我是没看懂。^_^&lt;/p&gt;
&lt;h2&gt;useContext基本用法&lt;/h2&gt;
&lt;p&gt;useContext(context)函数可以传入1个参数，该参数为共享数据对象的实例，useContext函数会返回该共享对象实例的value值。&lt;/p&gt;
&lt;h5&gt;代码形式&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import GlobalContext from &apos;./global-context&apos;; //引入共享数据对象

function Component(){
  const global = useContext(GlobalContext); //在函数组件中声明一个变量来代表该共享数据对象的value值

  //若想获取共享数据对象中的属性xxx的值，直接使用global.xxx即可
  return &amp;lt;div&amp;gt;
    {global.xxx}
  &amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;拆解说明&lt;/h5&gt;
&lt;p&gt;1、子组件(函数组件)需要先引入共享数据对象GlobalContext；&lt;br /&gt;
2、内部定义一个常量global，用来接收useContext函数返回GlobalContext的value值；&lt;br /&gt;
3、函数组件在return时，可以不使用&lt;code&gt;&amp;lt;GlobalCount.Customer /&amp;gt;&lt;/code&gt;标签，而是直接使用global.xx来获取共享数据；&lt;br /&gt;
4、请注意，这里执行的依然是单向数据流，只可以获取global.xx，不可以直接更改global.xx;&lt;/p&gt;
&lt;h4&gt;&apos;引入GlobalContext&apos;补充说明&lt;/h4&gt;
&lt;p&gt;示例中是通过import方式引入的，如果直接把GlobalContext定义在该组件内部，那不是就不用import了吗？&lt;br /&gt;
答：是的，你可以这么做。只不过定义在外部单独的模块中，各个组件都可以引用。&lt;/p&gt;
&lt;h4&gt;&apos;global&apos;补充说明&lt;/h4&gt;
&lt;p&gt;为了代码语义化，上述代码中使用到了global这个单词，但是请注意，该单词和原生JS中global(全局变量)无任何关联。实际项目中你可以使用任意具有语义的相关单词。比如定义用户共享数据你可以定义为UserContext、新闻共享数据你可以定义为NewsContext等。&lt;/p&gt;
&lt;h2&gt;useContext使用示例&lt;/h2&gt;
&lt;p&gt;举例：若某React组件一共由3层组件嵌套而成，从外到里分别是AppComponent、MiddleComponent、ChildComponent。AppComponent需要传递数据给ChildComponent。&lt;/p&gt;
&lt;p&gt;若使用useContext来实现，代码示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//global-context.js
import React from &apos;react&apos;;
const GlobalContext = React.createContext(); //请注意，这里还可以给React.createContext()传入一个默认值
//例如：const GlobalContext = React.createContext({name:&apos;Yang&apos;,age:18})
//假如&amp;lt;GlobalContext.Provider&amp;gt;中没有设置value的值，就会使用上面定义的默认值
export default GlobalContext;

...

//component.js
import React, { useContext } from &apos;react&apos;;
import GlobalContext from &apos;./global-context&apos;;

function AppComponent() {
  //标签&amp;lt;GlobalContext.Provider&amp;gt;中向下传递数据，必须使用value这个属性，且数据必须是键值对类型的object
  //如果不添加value，那么子组件获取到的共享数据value值是React.createContext(defaultValues)中的默认值defaultValues
  return &amp;lt;div&amp;gt;
    &amp;lt;GlobalContext.Provider value={{name:&apos;puxiao&apos;,age:34}}&amp;gt;
        &amp;lt;MiddleComponent /&amp;gt;
    &amp;lt;/GlobalContext.Provider&amp;gt;
  &amp;lt;/div&amp;gt;
}

function MiddleComponent(){
  //MiddleComponent 不需要做任何 “属性数据传递接力”，因此降低该组件数据传递复杂性，提高组件可复用性
  return &amp;lt;div&amp;gt;
    &amp;lt;ChildComponent /&amp;gt;
  &amp;lt;/div&amp;gt;
}

function ChildComponent(){
  const global = useContext(GlobalContext); //获取共享数据对象的value值
  //忘掉&amp;lt;GlobalContext.Consumer&amp;gt;标签，直接用global获取需要的值
  return &amp;lt;div&amp;gt;
    {global.name} - {global.age}
  &amp;lt;/div&amp;gt;
}

export default AppComponent;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假如ChildComponent不使用useContext，而是使用&lt;code&gt;&amp;lt;GlobalContext.Consumer&amp;gt;&lt;/code&gt;标签，那么代码相应修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function ChildComponent(){
  return &amp;lt;GlobalContext.Consumer&amp;gt;
    {
        global =&amp;gt; {
            return &amp;lt;div&amp;gt;{global.name} - {global.age}&amp;lt;/div&amp;gt;
        }
    }
  &amp;lt;/GlobalContext.Consumer&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用useContext可以大大降低获取数据代码复杂性。&lt;/p&gt;
&lt;p&gt;请注意：useContext只是简化了获取共享数据value的代码，但是对于&lt;code&gt;&amp;lt;XxxContext.Provider&amp;gt;&lt;/code&gt;的使用没有做任何改变，如果组件需要设置2个XxxContext，那么依然需要进行&lt;code&gt;&amp;lt;XxxContext.Provider&amp;gt;&lt;/code&gt;嵌套。&lt;/p&gt;
&lt;p&gt;上述代码中AppComponent只向下传递出去1个共享数据对象value值，那如果需要同时传递多个共享数据对象的value值，那该如何实现？&lt;br /&gt;
关于这个问题，会在 useContext高级用法中讲解。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useContext基础用法已经讲完。&lt;/p&gt;
</content:encoded></item><item><title>05 useEffect高级用法</title><link>https://blog.wemang.com/posts/web/react/useeffect%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/useeffect%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95/</guid><pubDate>Wed, 06 Sep 2023 17:47:16 GMT</pubDate><content:encoded>&lt;h1&gt;05 useEffect高级用法&lt;/h1&gt;
&lt;p&gt;所谓高级用法，只不过是一些深层知识点和实用技巧，你甚至可以把本章当做对前面知识点的一个巩固和学习。&lt;/p&gt;
&lt;h2&gt;让useEffect只在挂载后和卸载前执行一次&lt;/h2&gt;
&lt;p&gt;让我们实现 “04 useEffect基础用法” 中 举例2 提到的功能。&lt;/p&gt;
&lt;p&gt;组件需求：&lt;br /&gt;
1、若某类组件中有变量a，默认值为0，当组件第一次被挂载后或组件重新渲染后，将网页标题显示为a的值。&lt;br /&gt;
2、当组件第一次被挂载后执行一个自动累加器 setInterval，每1秒 a 的值+1。为了防止内存泄露，我们在该组件即将被卸载前清除掉该累加器。&lt;/p&gt;
&lt;p&gt;需求分析：&lt;br /&gt;
关于自动累加器的操作，只关联 “组件挂载后和组件卸载前” 这2个生命周期函数中，那useEffect还包含了每次组件重新渲染后，这该怎么办？&lt;/p&gt;
&lt;p&gt;答：useEffect函数的第2个参数表示该依赖关系，&lt;strong&gt;将useEffect的第2个参数，设置为空数组 []&lt;/strong&gt;，即表示告诉React，这个useEffect不依赖任何变量的更新所引发的组件重新渲染，以后此组件再更新也不需要调用此useEffect。&lt;/p&gt;
&lt;p&gt;这样就可以实现只在第一次挂载后和卸载前调用此useEffect的目的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React, { useState,useEffect} from &apos;react&apos;;

    function Component() {
      const [a, setA] = useState(0);//定义变量a，并且默认值为0

      //定义第1个useEffect，专门用来处理自动累加器
      useEffect(() =&amp;gt; {
        let timer = setInterval(() =&amp;gt; {setA(a+1)},1000);// &amp;lt;-- 请注意这行代码，暗藏玄机
        return () =&amp;gt; {
            clearInterval(timer);
        }
      }, []);//此处第2个参数为[]，告知React以后该组件任何更新引发的重新渲染都与此useEffect无关

      //定义第2个useEffect，专门用来处理网页标题更新
      useEffect(() =&amp;gt; {
        document.title = `${a} - ${Math.floor(Math.random()*100)}`;
      },[a])
      return &amp;lt;div&amp;gt; {a} &amp;lt;/div&amp;gt;
    }

    export default Component;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码实际运行正确吗？&lt;br /&gt;
答：不正确！&lt;/p&gt;
&lt;p&gt;？&lt;br /&gt;
小朋友，脸上是否有很多问号？？？&lt;/p&gt;
&lt;p&gt;实际运行会发现，当组件挂载后，确实会执行一次 &lt;code&gt;setA(a+1)&lt;/code&gt;，a 的值修改为了 1，然后... a 的值一直为 1，并没有继续累加。&lt;/p&gt;
&lt;p&gt;上述代码会收到react的一个错误警告提示：Either include it or remove the dependency array. You can also do a functional update &apos;setA(a =&amp;gt; ...)&apos; if you only need &apos;a&apos; in the &apos;setA&apos; call.&lt;br /&gt;
该错误警告意思是：如果你确认你传入的第2个参数是空数组，那么你可能会用到 &lt;code&gt;setA(a =&amp;gt; ...)&lt;/code&gt; 这种方式来更新a的值。&lt;/p&gt;
&lt;p&gt;问题出在哪里？&lt;/p&gt;
&lt;p&gt;让我们再看看那行有玄机的代码：&lt;br /&gt;
&lt;code&gt;let timer = setInterval(() =&amp;gt; {setA(a+1)},1000);&lt;/code&gt;
再看看 react 给我们的错误警告提示：You can also do a functional update &lt;code&gt;setA(a =&amp;gt; ...)&lt;/code&gt; if you only need &apos;a&apos; in the &apos;setA&apos; call.  你可能会用到 &lt;code&gt;setA(a =&amp;gt; ...)&lt;/code&gt; 这种方式来更新a的值。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;setA(a =&amp;gt; ...)&lt;/code&gt;  这是在 “03 useState高级用法”中，解决数据异步 时讲的更新方式。&lt;/p&gt;
&lt;p&gt;那我们就按照提示，将那行代码修改为：&lt;br /&gt;
&lt;code&gt;let timer = setInterval(() =&amp;gt; {setA(a =&amp;gt; a+1)},1000);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;再次执行，错误提示警告没有了，组件也完全按照我们的预期来执行了。react自带的语法检查真的好智能。&lt;/p&gt;
&lt;h5&gt;为什么会有这个问题？&lt;/h5&gt;
&lt;p&gt;关于刚才setInterval中累加 a 的值遇到的问题，React官方文档中也有类似示例，只不过他们用的变量是count，而我们这里用的变量是 a。&lt;/p&gt;
&lt;p&gt;我们看看从React官方文档中引用的话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有时候，你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state，但这通常会引起 Bug，传入空的依赖数组 []，意味着该 hook 只在组件挂载时运行一次，并非重新渲染时。但如此会有问题，在 setInterval 的回调中，count 的值不会发生变化。因为当 effect 执行时，我们会创建一个闭包，并将 count 的值被保存在该闭包当中，且初值为 0。每隔一秒，回调就会执行 setCount(0 + 1)，因此，count 永远不会超过 1。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;再次重复一遍：如果useEffect函数第2个参数为空数组，那么react会将该useEffect的第1个参数 effect 建立一个闭包，该闭包里的变量 a 被永远设定为当初的值，即 0。尽管setInterval正常工作，每次都“正常执行了”，可是 &lt;code&gt;setA(a+1)&lt;/code&gt;中 a 的值一直没变化，一直都是当初的0，所以造成 0 + 1 一直都等于 1 的结果。&lt;/p&gt;
&lt;p&gt;而如果修改成 &lt;code&gt;setA(a =&amp;gt; a+1)&lt;/code&gt; 的形式，那么就解决了 a 数据异步的问题，每次都是读取最新当前 a 的值。&lt;/p&gt;
&lt;p&gt;这个点是使用 useEffect 很容易掉进去的一个坑，切记切记。&lt;/p&gt;
&lt;p&gt;或者以后养成都用异步更新数据的习惯。&lt;/p&gt;
&lt;h2&gt;性能优化&lt;/h2&gt;
&lt;p&gt;通过上面的例子，我们其实已经实现了 前文中 举例2 和举例3 的效果。&lt;/p&gt;
&lt;p&gt;咦~ 刚才讲的是举例2，没有将举例3啊... 因为举例3中提到的类组件中有多个变量数据，在函数组件中这个问题本身是靠useState来解决的，跟useEffect无关。&lt;/p&gt;
&lt;p&gt;接下来讲一下useEffect函数第2个参数提高性能的正确用法。&lt;/p&gt;
&lt;p&gt;举例：若一个组件中有一个自定义变量obj，obj有两个属性a、b，当a发生变化时，网页标题也跟着a发生变化。&lt;br /&gt;
补充说明：&lt;br /&gt;
1、我们为了让a、b都可以发生变化，将在组件中创建2个按钮，点击之后分别可以修改a、b的值；&lt;br /&gt;
2、为了更加清楚看到每次渲染，我们在网页标题中 a 的后面再增加一个随机数字；&lt;/p&gt;
&lt;p&gt;我们首先看以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React, { useState,useEffect} from &apos;react&apos;;
    function Component() {
      const [obj,setObj] = useState({a:0,b:0});
      useEffect(() =&amp;gt; {
        document.title = `${obj.a} - ${Math.floor(Math.random()*50)}`;
      }); //注意此时我们并未设置useEffect函数的第2个参数

      //如果下面代码看不懂，你需要重新去温习useState高级用法中的“数据类型为Objcet，修改方法”
      return &amp;lt;div&amp;gt;
        {JSON.stringify(obj)}
        &amp;lt;button onClick={() =&amp;gt; {setObj({...obj,a:obj.a+1})}}&amp;gt;a+1&amp;lt;/button&amp;gt; 
        &amp;lt;button onClick={() =&amp;gt; {setObj({...obj,b:obj.b+1})}}&amp;gt;b+1&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    }
    export default Component;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于我们在网页标题中添加了随机数，因此实际运行你会发现即使修改b的值，也会引发网页标题重新“变更一次”。&lt;/p&gt;
&lt;p&gt;理由显而易见，修改b的值也会触发组件重新渲染，进而触发useEffect中的代码。&lt;/p&gt;
&lt;p&gt;正确的做法应该是我们给useEffect添加上第2个参数：[obj.a]，明确告诉React，只有当obj.a变更引发的重新渲染才执行本条useEffect。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    useEffect(() =&amp;gt; {
       document.title = `${obj.a} - ${Math.floor(Math.random()*50)}`;
     },[obj.a]); //第2个参数为数组，该数组中可以包含多个变量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加过[obj.a]之后，再次运行，无论obj.b或者其他数据变量引发的组件重新渲染，都不会执行该useEffect。&lt;/p&gt;
&lt;p&gt;因此达到提高性能的目的。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useEffect高级用法已经讲完，相信useState和useEffect的组合使用，已经能够让你写出一些简单的React Hook 组件。&lt;/p&gt;
&lt;p&gt;接下来学习第3个Hook函数useContext。&lt;/p&gt;
</content:encoded></item><item><title>04 useEffect基础用法</title><link>https://blog.wemang.com/posts/web/react/useeffect%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/useeffect%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</guid><pubDate>Wed, 06 Sep 2023 17:47:10 GMT</pubDate><content:encoded>&lt;h1&gt;04 useEffect基础用法&lt;/h1&gt;
&lt;h2&gt;useEffect概念解释&lt;/h2&gt;
&lt;p&gt;我们第二个要学习的Hook(钩子函数)是useEffect，他的作用是“勾住”函数组件中某些生命周期函数。&lt;/p&gt;
&lt;p&gt;都能勾住哪些生命周期函数？&lt;br /&gt;
答：componentDidMount(组件被挂载完成后)、componentDidUpdate(组件重新渲染完成后)、componentWillUnmount(组件即将被卸载前)&lt;/p&gt;
&lt;p&gt;为什么是这3个生命周期函数？&lt;br /&gt;
答：因为修改数据我们可以使用前面学到的useState，数据变更会触发组件重新渲染，上面3个就是和组件渲染关联最紧密的生命周期函数。&lt;/p&gt;
&lt;p&gt;那其他生命周期函数呢？&lt;br /&gt;
答：该问题的回答，引用&lt;a href=&quot;https://react.docschina.org/docs/hooks-faq.html#do-hooks-cover-all-use-cases-for-classes&quot;&gt;React官方中文文档FAQ&lt;/a&gt;，如下&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们给 Hook 设定的目标是尽早覆盖 class 的所有使用场景。目前暂时还没有对应不常用的 getSnapshotBeforeUpdate，getDerivedStateFromError 和 componentDidCatch 生命周期的 Hook 等价写法，但我们计划尽早把它们加进来。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;useEffect是来解决类组件什么问题的？&lt;/h2&gt;
&lt;p&gt;答：useEffect是来解决类组件 &lt;strong&gt;某些执行代码被分散在不同的生命周期函数中&lt;/strong&gt; 的问题。&lt;/p&gt;
&lt;p&gt;举例1：若某类组件中有变量a，默认值为0，当组件第一次被挂载后或组件重新渲染后，将网页标题显示为a的值。&lt;br /&gt;
那么在类组件里，我们需要写的代码是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //为了更加清楚看到每次渲染，我们在网页标题中 a 的后面再增加一个随机数字
    componentDidMount(){
        document.title = `${this.state.a} - ${Math.floor(Math.random()*100)}`;
    }
    componentDidUpdate(){
        document.title = `${this.state.a} - ${Math.floor(Math.random()*100)}`;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面这种代码里你会看到，为了保证第一次被挂载、组件重新渲染后都执行修改网页标题的行为，相同的代码我们需要分别在componentDidMount、componentDidUpdate中写2次。&lt;/p&gt;
&lt;p&gt;举例2：假设需要给上面那个组件新增一个功能，当组件第一次被挂载后执行一个自动累加器 setInterval，每1秒 a 的值+1。为了防止内存泄露，我们在该组件即将被卸载前清除掉该累加器。&lt;br /&gt;
那么在类组件里，我们需要写的代码是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    timer = null;//新增一个可内部访问的累加器变量(注：类组件定义属性时前面无法使用 var/let/const)
    componentDidMount(){
        document.title = `${this.state.a} - ${Math.floor(Math.random()*100)}`;
        this.timer = setInterval(() =&amp;gt; {this.setState({a:this.state.a+1})}, 1000);//添加累加器
    }
    componentDidUpdate(){
        document.title = `${this.state.a} - ${Math.floor(Math.random()*100)}`; 
    }
    componentWillUnmount(){
        clearInterval(this.timer);//清除累加器
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上面代码可以看到，增加累加器和清除累加器这2个相关的执行代码被分别定义在componentDidMount、componentWillUnmount这两个生命周期函数中。&lt;/p&gt;
&lt;p&gt;举例3：假设给上面的组件再新增一个变量 b，当 b 的值发生变化后也会引发组件重新渲染，然后呢？有什么隐患吗？&lt;br /&gt;
答：b 的值改变引发组件重新渲染，然后肯定是会触发componentDidUpdate函数，这时会让修改网页标题的代码再次执行一次，尽管此时a的值并没有发生任何变化。&lt;/p&gt;
&lt;p&gt;再来回顾一下上面的3个例子：&lt;br /&gt;
1、举例1中，相同的代码可能需要在不同生命周期函数中写2次；&lt;br /&gt;
2、举例2中，相关的代码可能需要在不同生命周期函数中定义；&lt;br /&gt;
3、举例3中，无论是哪个原因引发的组件重新渲染，都会触发生命周期函数的执行，造成一些不必要的代码执行；&lt;/p&gt;
&lt;p&gt;以上就是 类组件“某些执行代码被分散在不同的生命周期函数中”引发的问题具体表现，而useEffect就是来解决这些问题的。&lt;/p&gt;
&lt;p&gt;接下来开始学习useState。&lt;/p&gt;
&lt;h2&gt;useEffect函数源码：&lt;/h2&gt;
&lt;p&gt;回到useEffect的学习中，首先看一下React源码中的&lt;a href=&quot;https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js&quot;&gt;ReactHooks.js&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //备注：源码采用TypeScript编写，如果不懂TS代码，阅读起来稍显困难
    export function useEffect(
      create: () =&amp;gt; (() =&amp;gt; void) | void,
      deps: Array&amp;lt;mixed&amp;gt; | void | null,
    ): void {
      const dispatcher = resolveDispatcher();
      return dispatcher.useEffect(create, deps);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码看不懂没关系，本系列教程只是讲述“如何使用Hook”，并不是“Hook源码分析”。之所以贴出源码只是为了让你以后也可以给面试官吹嘘你读过React源码。^_^&lt;/p&gt;
&lt;h2&gt;useEffect基本用法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;useEffect(effect,[deps])&lt;/code&gt;函数可以传入2个参数，第1个参数为我们定义的执行函数、第2个参数是依赖关系(可选参数)。若一个函数组件中定义了多个useEffect，那么他们实际执行顺序是按照在代码中定义的先后顺序来执行的。&lt;/p&gt;
&lt;p&gt;具体说明如下：&lt;br /&gt;
第1个值effect是一个function，用来编写useEffect对应的执行代码。&lt;br /&gt;
还记得本文开头提到的useEffect能勾住哪3个生命周期函数吗？&lt;br /&gt;
componentDidMount、componentDidUpdate、componentWillUnmount ，当上述3个生命周期函数执行后，就会触发useEffect函数，进而执行而第1个参数 effect 中的内容。&lt;/p&gt;
&lt;p&gt;组件挂载后(componentDidMount)与组件重新渲染后(componentDidUpdate)对应的代码合并为一个函数这个容易理解，可是组件卸载前(componentWillUnmount)也能融入进来？&lt;br /&gt;
答：是的，通过在 effect 中 return 一个函数来实现的。&lt;/p&gt;
&lt;p&gt;关于第2个参数 [deps] ，先知道这个是可选参数，是Hook用来向React表明useEffect依赖关系的即可。关于它的用法会在useEffect高级用法中有更多详细讲述。&lt;/p&gt;
&lt;h5&gt;代码形式：&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;    useEffect(() =&amp;gt; {
        //此处编写 组件挂载之后和组件重新渲染之后执行的代码
        ...

        return () =&amp;gt; {
            //此处编写 组件即将被卸载前执行的代码
            ...
        }
    },[deps])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之前说过useEffect第1个参数 effect 是个 function，只是这个 function 稍显复杂。&lt;/p&gt;
&lt;h5&gt;拆解说明：&lt;/h5&gt;
&lt;p&gt;1、effect 函数主体内容中的代码，就是组件挂载之后和组件重新渲染之后你需要执行的代码；&lt;br /&gt;
2、effect 函数 return 出去的返回函数主体内容中的代码，就是组件即将被卸载前你需要执行的代码；&lt;br /&gt;
3、第2个参数 [deps]，为可选参数，若有值则向React表明该useEffect是依赖哪些变量发生改变而触发的；&lt;/p&gt;
&lt;h4&gt;&apos;effect&apos;补充说明&lt;/h4&gt;
&lt;p&gt;1、若你不需要在组件卸载前执行任何代码，那么可以忽略不写 effect 中的 return相关代码；&lt;/p&gt;
&lt;h5&gt;&apos;[deps]&apos;补充说明：&lt;/h5&gt;
&lt;p&gt;1、若缺省，则组件挂载、组件重新渲染、组件即将被卸载前，每一次都会触发该useEffect；&lt;br /&gt;
3、若传值，则必须为数组，数组的内容是函数组件中通过useState自定义的变量或者是父组件传值过来的props中的变量，告诉React只有数组内的变量发生变化时才会触发useEffect；&lt;br /&gt;
4、若传值，但是传的是空数组 []，则表示该useEffect里的内容仅会在“挂载完成后和组件即将被卸载前”执行一次；&lt;/p&gt;
&lt;h2&gt;useEffect使用示例：&lt;/h2&gt;
&lt;p&gt;还记得本文上面关于 类组件“某些执行代码被分散在不同的生命周期函数中”引发的问题时，所举的3个例子吗？&lt;br /&gt;
我们用Hook来依次分别实现举例1、举例2、举例3，通过3个功能的代码示例，让你明白useEffect的具体用法。&lt;/p&gt;
&lt;p&gt;举例1：若某类组件中有变量a，默认值为0，当组件第一次被挂载后或组件重新渲染后，将网页标题显示为a的值。&lt;br /&gt;
补充说明：&lt;br /&gt;
1、为了让 a 的值可以发生变化，我们在组件中添加一个按钮，每次点击 a 的值 +1&lt;br /&gt;
2、为了更加清楚看到每次渲染，我们在网页标题中 a 的后面再增加一个随机数字&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React, { useState,useEffect} from &apos;react&apos;;

    function Component() {
      const [a, setA] = useState(0);//定义变量a，并且默认值为0
      useEffect(() =&amp;gt; {
          //无论是第一次挂载还是以后每次组件更新，修改网页标题的执行代码只需要在这里写一次即可
          document.title = `${a} - ${Math.floor(Math.random()*100)}`;
      })
      const clickAbtHandler = (eve) =&amp;gt;{
          setA(a+1);
      }
      return &amp;lt;div&amp;gt;
          {a}
          &amp;lt;button onClick={clickAbtHandler}&amp;gt;a+1&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    }

    export default Component;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上述代码可以看出，“类组件中相同的代码可能需要在不同生命周期函数中写2次”这个问题已通过Hook useEffect已解决。&lt;/p&gt;
&lt;p&gt;这里只是实现列 举例1 中的功能，是useEffect最基础的用法。举例2、举例3 中的功能实现我们放到 useEffect 高级用法 中来讲解。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useEffect基础用法已经讲完。&lt;/p&gt;
</content:encoded></item><item><title>03 useState高级用法</title><link>https://blog.wemang.com/posts/web/react/usestate%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/usestate%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95/</guid><pubDate>Wed, 06 Sep 2023 14:37:11 GMT</pubDate><content:encoded>&lt;h1&gt;03 useState高级用法&lt;/h1&gt;
&lt;p&gt;所谓高级用法，只不过是一些深层知识点和实用技巧，你甚至可以把本章当做对前面知识点的一个巩固和学习。&lt;/p&gt;
&lt;h2&gt;恢复默认值&lt;/h2&gt;
&lt;p&gt;组件需求：实现一个计数器，有3个按钮，点击后分别实现：恢复默认值、点击+1、点击-1&lt;/p&gt;
&lt;p&gt;实现代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React, { useState } from &apos;react&apos;;
    
    function Component() {
      const initCount = 0;
      const [count, setCount] = useState(initCount);
    
      return &amp;lt;div&amp;gt;
        {count}
        &amp;lt;button onClick={() =&amp;gt; {setCount(initCount)}}&amp;gt;init&amp;lt;/button&amp;gt;
        &amp;lt;button onClick={() =&amp;gt; {setCount(count+1)}}&amp;gt;+1&amp;lt;/button&amp;gt;
        &amp;lt;button onClick={() =&amp;gt; {setCount(count-1)}}&amp;gt;-1&amp;lt;/button&amp;gt;
      &amp;lt;/div&amp;gt;
    }
    
    export default Component;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码分析：&lt;br /&gt;
1、通过额外定义一个变量initCount=0，作为count的默认值；&lt;br /&gt;
2、任何时候想恢复默认值，直接将initCount赋值给count；&lt;/p&gt;
&lt;h2&gt;解决数据异步&lt;/h2&gt;
&lt;p&gt;还是基于上面那个示例，假设现在新增1个按钮，点击该按钮后执行以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    for(let i=0; i&amp;lt;3; i++){
      setCount(count+1);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过for循环，执行了3次setCount(count+1)，那么你觉得count会 +3 吗？&lt;br /&gt;
答案是：肯定不会&lt;/p&gt;
&lt;p&gt;无论for循环执行几次，最终实际结果都将是仅仅执行一次 +1。&lt;/p&gt;
&lt;p&gt;为什么？&lt;br /&gt;
类组件中setState赋值过程是异步的，同样在Hook中 setXxx 赋值也是异步的，比如上述代码中的setCount。&lt;/p&gt;
&lt;p&gt;虽然执行了3次setCount(count+1)，可是每一次修改后的count并不是立即生效的。当第2次和第3次执行时获取到count的值和第1次获取到的count值是一样的，所以最终其实相当于仅执行了1次。&lt;/p&gt;
&lt;h5&gt;解决办法：&lt;/h5&gt;
&lt;p&gt;你肯定第一时间想到的是这样解决方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    let num = count;
    for(let i=0; i&amp;lt;3; i++){
      num +=1;
    }
    setCount(num);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做肯定没问题，只不过有更简便、性能更高的方式。&lt;/p&gt;
&lt;p&gt;和类组件中解决异步的办法类似，就是不直接赋值，而是采用“箭头函数返回值的形式”赋值。&lt;/p&gt;
&lt;p&gt;把代码修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    for(let i=0; i&amp;lt;3; i++){
      setCount(prevData =&amp;gt; {return prevData+1});
      //可以简化为 setCount(prevData =&amp;gt; prevData+1);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码分析：&lt;br /&gt;
1、prevData为我们定义的一个形参，指当前count应该的值；&lt;br /&gt;
2、&lt;code&gt;{return prevData+1}&lt;/code&gt; 中，将 prevData+1，并将运算结果return出去。当然也非常推荐使用更加简化的写法：&lt;code&gt;setCount(prevData =&amp;gt; prevData+1)&lt;/code&gt;；&lt;br /&gt;
3、最终将prevData赋值给count；&lt;/p&gt;
&lt;p&gt;补充说明：你可以将prevData修改成任意你喜欢的变量名称，比如prev，只需要确保和后面return里的一致即可。&lt;/p&gt;
&lt;h2&gt;数据类型为Objcet的修改方法&lt;/h2&gt;
&lt;p&gt;之前的示例中，每个useState对应的值都是简单的string或number，如果对应的值是object，又该如何处理呢？&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`const [person, setPerson] = useState({name:&apos;puxiao&apos;,age:34});`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若想将age的值修改为18，该怎么写？&lt;/p&gt;
&lt;p&gt;如果你有类组件编程经验，你肯定第一时间想是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`setPerson({age:18});`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在类组件中，setState是执行的是“异步对比累加赋值”，何为“对比”？  就是先对比之前数据属性中是否有age，如果有则修改age值，同时不会影响到其他属性的值。我猜测react是使用ES6中新增加的Object.assign()这个函数来实现这一步的。&lt;/p&gt;
&lt;p&gt;但是，用useState定义的修改函数 setXxxx，例如setPerson中，执行的是 “异步直接赋值”。&lt;/p&gt;
&lt;p&gt;请看实际执行的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    console.log(person);//{name:&apos;puxiao&apos;,age:34}
    setPerson({age:18});
    console.log(person);//{age:18}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没错，虽然只是希望修改age的值，但是由于是“直接赋值”，导致&lt;code&gt;{age:18}&lt;/code&gt;替换了整个&lt;code&gt;{name:&apos;puxiao&apos;,age:34}&lt;/code&gt;&lt;/p&gt;
&lt;h5&gt;正确的做法：&lt;/h5&gt;
&lt;p&gt;我们需要先将person拷贝一份，修改之后再进行赋值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    let newData = {...person};
    newData.age = 18;
    setPerson(newData);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码还有一种简写形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`setPerson({...person,age:18}); //这种简写是解构赋值带来的，并不是React提供的`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码分析：&lt;br /&gt;
1、先通过...person，将原有person做一次解构，得到一份复制品(浅拷贝)；&lt;br /&gt;
2、修改age的值；&lt;br /&gt;
3、将修改过后的新数据，通过setPerson赋值给person；&lt;/p&gt;
&lt;p&gt;完整示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React, { useState } from &apos;react&apos;;

    function Component() {

      const [person, setPerson] = useState({name:&apos;puxiao&apos;,age:34});

      const nameChangeHandler = (eve) =&amp;gt; {
        setPerson({...person,name:eve.target.value});
      }

      const ageChangeHandler = (eve) =&amp;gt; {
        setPerson({...person,age:eve.target.value});
      }

      return &amp;lt;div&amp;gt;
        &amp;lt;input type=&apos;text&apos; value={person.name} onChange={nameChangeHandler} /&amp;gt;
        &amp;lt;input type=&apos;number&apos; value={person.age} onChange={ageChangeHandler} /&amp;gt;
        {JSON.stringify(person)}
      &amp;lt;/div&amp;gt;
    }
    export default Component;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数据类型为Array的修改方法&lt;/h2&gt;
&lt;p&gt;和数据类型为Object相似，都是需要通过先拷贝一次，修改后再整体赋值。&lt;/p&gt;
&lt;p&gt;这里举一个简单的小例子，以下代码实现了一个类似学习计划列表的功能组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import React, { useState } from &apos;react&apos;;

    function Component() {

      const [str, setStr] = useState(&apos;&apos;);
      const [arr, setArr] = useState([&apos;react&apos;, &apos;Koa&apos;]);

      const inputChangeHandler = (eve) =&amp;gt; {
        setStr(eve.target.value);
      }

      const addHeadHandler = (eve) =&amp;gt; {
        setArr([str,...arr]);//添加至头
        setStr(&apos;&apos;);
      }

      const addEndHandler = (eve) =&amp;gt; {
        setArr([...arr, str]);//添加至尾
        setStr(&apos;&apos;);
      }

      const delHeadHandler = (eve) =&amp;gt; {
        let new_arr = [...arr];
        new_arr.shift();//从头删除1项目
        setArr(new_arr);
      }

      const delEndHandler = (eve) =&amp;gt; {
        let new_arr = [...arr];
        new_arr.pop();//从尾删除1项目
        setArr(new_arr);
      }

      const delByIndex = (eve) =&amp;gt; {
        let index = eve.target.attributes.index.value;
        let new_arr = [...arr];
        new_arr.splice(index,1);//删除当前项
        setArr(new_arr);
      }

      return &amp;lt;div&amp;gt;
        &amp;lt;input type=&apos;text&apos; value={str} onChange={inputChangeHandler} /&amp;gt;
        &amp;lt;button onClick={addHeadHandler} &amp;gt;添加至头&amp;lt;/button&amp;gt;
        &amp;lt;button onClick={addEndHandler} &amp;gt;添加至尾&amp;lt;/button&amp;gt;
        &amp;lt;button onClick={delHeadHandler} &amp;gt;从头删除1项&amp;lt;/button&amp;gt;
        &amp;lt;button onClick={delEndHandler} &amp;gt;从尾删除1项&amp;lt;/button&amp;gt;
        &amp;lt;ul&amp;gt;
            {arr.map(
                (item, index) =&amp;gt; {
                    return &amp;lt;li key={`item${index}`}&amp;gt;学习{index} -  {item}
                        &amp;lt;span index={index} onClick={delByIndex} style={{ cursor: &apos;pointer&apos; }}&amp;gt;删除&amp;lt;/span&amp;gt;
                    &amp;lt;/li&amp;gt;
                }
            )}
        &amp;lt;/ul&amp;gt;
      &amp;lt;/div&amp;gt;
    }

    export default Component;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;性能优化&lt;/h2&gt;
&lt;p&gt;通过 setXxx 设置新值，但是如果新值和当前值完全一样，那么会引发React重新渲染吗？&lt;/p&gt;
&lt;p&gt;通过React官方文档可以知道，当使用 setXxx 赋值时，Hook会使用Object.is()来对比当前值和新值，结果为true则不渲染，结果为false就会重新渲染。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    let str=&apos;a&apos;;
    Object.is(str,&apos;a&apos;); //true

    let str=&apos;18&apos;;
    Object.is(str,18); //str为String类型，18为Number类型，因此结果为false
    
    let obj={name:&apos;a&apos;};
    Object.is(obj,{name:&apos;a&apos;}); //false
    //虽然obj和{name:&apos;a&apos;}字面上相同，但是obj==={name:&apos;a&apos;}为false，并且在Object.is()运算下认为两者不是同一个对象
    //事实上他们确实不是同一个对象，他们各自占用了一份内存

    let obj={name:&apos;a&apos;};
    let a=obj;
    let b=obj;
    Object.is(a,b); //因为a和b都指向obj，因此结果为true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由上面测试可以看出：&lt;br /&gt;
1、对于简单类型的值，例如String、Number 新旧值一样的情况下是不会引起重新渲染的；&lt;br /&gt;
2、对于复杂类型的值，即使新旧值 “看上去是一样的” 也会引起重新渲染。除非新旧值指向同一个对象，或者可以说成新旧值分别是同一个对象的引用；&lt;/p&gt;
&lt;p&gt;采用复杂类型的值不是不可以用，很多场景下都需要用到，但是请记得上面的测试结果。&lt;/p&gt;
&lt;p&gt;为了可能存在的性能问题，如果可以，最好避免使用复杂类型的值。&lt;/p&gt;
&lt;h2&gt;自定义Hook&lt;/h2&gt;
&lt;p&gt;所谓自定义Hook，就是将Hook函数从函数组件中抽离，抽离之后多个函数组件可以共用该自定义Hook，共享该Hook的逻辑。&lt;/p&gt;
&lt;p&gt;因为目前仅学习了useState，再多学习几个Hook函数后，会单独拿出一个篇章来讲解如何自定义Hook。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useState高级用法已经讲完，相信你已经掌握了useState的使用方法。&lt;/p&gt;
&lt;p&gt;useState是本系列文章讲解的第一个Hook函数，同时也是使用频率最高的Hook，甚至可以说useState是函数组件开发的基石，因此本章稍显啰嗦，但目的就是希望你能理解透彻。&lt;/p&gt;
&lt;p&gt;在后面讲解其他Hook函数时，将会尽量使用简洁、高冷的文章风格。&lt;/p&gt;
&lt;p&gt;接下来学习第2个Hook函数useEffect。&lt;/p&gt;
</content:encoded></item><item><title>02 useState基础用法</title><link>https://blog.wemang.com/posts/web/react/usestate%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</link><guid isPermaLink="true">https://blog.wemang.com/posts/web/react/usestate%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95/</guid><pubDate>Wed, 06 Sep 2023 14:37:04 GMT</pubDate><content:encoded>&lt;h1&gt;02 useState基础用法&lt;/h1&gt;
&lt;h2&gt;useState概念解释&lt;/h2&gt;
&lt;p&gt;我们第一个要学习的Hook(钩子函数)是useState，他的作用是“勾住”函数组件中自定义的变量。&lt;/p&gt;
&lt;p&gt;“勾住”？&lt;br /&gt;
回顾一下 “React Hook 简介” 文中那句话：Hook本身单词意思是“钩子”，作用就是“勾住某些生命周期函数或某些数据状态，并进行某些关联触发调用”。&lt;/p&gt;
&lt;p&gt;“如何勾住”？
在React底层代码中，是通过自定义dispatcher，采用“发布订阅模式”实现的。&lt;/p&gt;
&lt;p&gt;关于“钩子”、“勾住”、“如何勾住”的概念以后在学习其他Hook函数时不再做解释。&lt;/p&gt;
&lt;h2&gt;useState是来解决类组件什么问题的？&lt;/h2&gt;
&lt;p&gt;答：useState能够解决类组件 &lt;strong&gt;所有自定义变量只能存储在this.state&lt;/strong&gt; 的问题。&lt;/p&gt;
&lt;p&gt;举例：若某组件需要有2个自定义变量name和age，那么在类组件中只能如下定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    constructor(props) {
        super(props);
        this.state = {
          name:&apos;puxiao&apos;,
          age:34
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;name和age只能作为this.state的一个属性。&lt;/p&gt;
&lt;p&gt;没有对比就没有伤害，看一下使用useState后，函数组件是如何实现上述需求的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    const [name,setName] = useState(&apos;puxiao&apos;);
    const [age,setAge] = useState(34);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1、函数组件本身是一个函数，不是类，因此没有构造函数constructor(props)；&lt;br /&gt;
2、任何你想定义的变量都可以单独拆分出去，独立定义，互不影响；&lt;/p&gt;
&lt;p&gt;两段代码对比之下，你就会发现使用Hook的useState后，会让我们定义的变量相对独立，清晰简单，便于管理。&lt;/p&gt;
&lt;p&gt;接下来开始学习useState。&lt;/p&gt;
&lt;h2&gt;useState函数源码：&lt;/h2&gt;
&lt;p&gt;首先看一下React源码中的&lt;a href=&quot;https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js&quot;&gt;ReactHooks.js&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    //备注：源码采用TypeScript编写，如果不懂TS代码，阅读起来稍显困难
    export function useState&amp;lt;S&amp;gt;(
      initialState: (() =&amp;gt; S) | S,
    ): [S, Dispatch&amp;lt;BasicStateAction&amp;lt;S&amp;gt;&amp;gt;] {
      const dispatcher = resolveDispatcher();
      return dispatcher.useState(initialState);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码看不懂没关系，本系列教程只是讲述“如何使用Hook”，并不是“Hook源码分析”。之所以贴出源码只是为了显得本文比较有深度。^_^&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;更新于2020.11.10，这里强调一下：React 源码中使用的是 flow 语法，根本不是 TypeScript 语法，只不过 2 者实在是太像了，以至于让我之前一直误以为 React 源码中是 TS。不过你完全可以将 TS 的泛型知识去套用到 flow 中。在此特别说明一下，至于后续章节中就不再做提醒和修改了，你就当成 TS 语法去理解也行。&lt;/strong&gt;&lt;/p&gt;
&lt;h5&gt;补充一些TypeScript常识：&lt;/h5&gt;
&lt;p&gt;1、react 本身采用TypeScript编写，还是补充点TS常识，方便对各个 hook 函数源码的理解。&lt;br /&gt;
2、对于useState以及以后要学习的其他hook函数源码，函数参数中会反复出现&lt;code&gt;&amp;lt;S\&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;T\&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;P\&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;I\&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;I\&amp;gt;&lt;/code&gt;，这些大写字母，react约定他们对应的单词如下：&lt;br /&gt;
&lt;code&gt;state -&amp;gt; S -&amp;gt;&lt;/code&gt; 约定表示某种“数据”&lt;br /&gt;
&lt;code&gt;type -&amp;gt; T -&amp;gt;&lt;/code&gt; 约定表示某种“类型”&lt;br /&gt;
&lt;code&gt;props -&amp;gt; P -&amp;gt;&lt;/code&gt; 约定表示“属性传值对应的props”&lt;br /&gt;
&lt;code&gt;initial -&amp;gt; I -&amp;gt;&lt;/code&gt; 约定表示某个“初始值”&lt;/p&gt;
&lt;p&gt;1、这种用&lt;code&gt;&amp;lt;X\&amp;gt;&lt;/code&gt;包裹起来的类型声明，在TS中成为“泛型”。理论上是可以使用任意单词的，上面那些缩写只是react自己约定单词缩写。&lt;br /&gt;
2、对于一段TS代码，如果出现了&lt;code&gt;&amp;lt;S\&amp;gt;&lt;/code&gt;，那么后面所有的&lt;code&gt;&amp;lt;S\&amp;gt;&lt;/code&gt;都将表示“某种相同类型的数据”。对于TypeScript的泛型相关知识，请自己百度学习。&lt;/p&gt;
&lt;h2&gt;useState基本用法&lt;/h2&gt;
&lt;p&gt;useState(value)函数会返回一个数组，该数组包含2个元素：第1个元素为我们定义的变量，第2个元素为修改该变量对应的函数名称。&lt;/p&gt;
&lt;h5&gt;代码形式：&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;const [variable,setVariable] = useState(value);
//....
setVariable(newValue);//修改variable的值
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;拆解说明：&lt;/h5&gt;
&lt;p&gt;1、&lt;code&gt;const [a,b] = [a,b]&lt;/code&gt; 这种形式为ES6的“解构赋值”；&lt;br /&gt;
2、&apos;variable&apos;为函数组件中自定义的变量名；&lt;br /&gt;
3、&apos;setVariable&apos;为修改&apos;variable&apos;对应的函数名；&lt;br /&gt;
4、&apos;useState&apos;为本次学习的Hook函数；&lt;br /&gt;
5、&apos;value&apos;为变量默认值&lt;br /&gt;
6、&apos;setVariable(newValue)&apos;为调用setVariable并将新的值newValue赋值给variable；&lt;/p&gt;
&lt;h4&gt;&apos;variable&apos;补充说明&lt;/h4&gt;
&lt;p&gt;1、variable为变量名，实际使用中可以修改成任意变量名，比如name、age、count等等；&lt;br /&gt;
2、但是，函数组件接收父级组件属性传值的变量名为props，因此建议你不要将变量名定为props，以免混淆；&lt;br /&gt;
3、我不听话，我就非要将变量名定义成props，那又会怎么样？答案是不会有什么问题，不仅不会报错而且还会正常执行。&lt;/p&gt;
&lt;h5&gt;&apos;setVariable&apos;补充说明：&lt;/h5&gt;
&lt;p&gt;1、该名称采用 &quot;set&quot;+&quot;变量名&quot; 的驼峰命名形式，只是为了提高代码可读性。&lt;br /&gt;
2、一般React项目都约定使用此种命名方式，所以推荐你也如此使用。&lt;br /&gt;
3、当然你也可以使用任意你喜欢的命名风格，但是切记不能以数字开头。&lt;/p&gt;
&lt;h5&gt;&apos;value&apos;补充说明：&lt;/h5&gt;
&lt;p&gt;1、必填项，不可缺省，若缺省则实际运行时会提示变量名未定义；&lt;br /&gt;
2、值的类型可以是字符串、数字、数组、对象；&lt;br /&gt;
3、值还可以为null，但不可以为undefined；&lt;/p&gt;
&lt;h5&gt;&apos;newValue&apos;补充说明(非常重要)：&lt;/h5&gt;
&lt;p&gt;setVariable采用 “异步直接赋值” 的形式，并不会像类组件中的setState()那样做“异步对比累加赋值”。&lt;/p&gt;
&lt;p&gt;“异步”？&lt;br /&gt;
这里的“异步”和类组件中setState中的异步是同一个意思，都是为了优化React渲染性能而故意为之。&lt;/p&gt;
&lt;p&gt;&quot;直接赋值&quot;？&lt;br /&gt;
1、在Hook中，对于简单类型数据，比如number、string类型，可以直接通过setVariable(newValue)直接进行赋值。&lt;br /&gt;
2、但对于复杂类型数据，比如array、object类型，若想修改其中某一个属性值而不影响其他属性，则需要先复制出一份，修改某属性后再整体赋值。具体如何做，请看下一篇“useState高级用法”中“数据类型为Objcet/Array修改方法”内容。&lt;/p&gt;
&lt;p&gt;如果新值和当前值完全一样，那么会引发React重新渲染吗？请看下一篇“useState高级用法”中“性能优化”内容。&lt;/p&gt;
&lt;p&gt;停！上面的信息量有点多，让我们把思绪先回到最基础的用法上。&lt;/p&gt;
&lt;h2&gt;useState使用示例：&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;    //函数组件内定义变量name
    const [name,setName] = useState(&apos;nodejs&apos;); //name默认值为nodejs
    
    //在函数组件内，某些事件交互处理函数中修改name的值，例如某次鼠标点击的处理函数handleClick
    const handleClick = () =&amp;gt; {
      setName(&apos;koa&apos;);
      //请注意，setName(&apos;koa&apos;)是异步修改的，如果此时执行console.log(name) 输出的值依然是nodejs
      //请留意下一篇文章 “03 useState高级用法” 中 “解决数据异步” 相关部分
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码中，我们进行了以下操作：&lt;br /&gt;
1、声明一个变量name、修改name的方法setName、并将name默认值设置为&apos;nodejs&apos;；&lt;br /&gt;
2、通过setName将name值修改为&apos;koa&apos;；&lt;/p&gt;
&lt;p&gt;注意：在一个组件中，可以不限次数使用useState()，因此，我们可以声明多个变量，例如下面代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    const [name,setName] = useState(&apos;puxiao&apos;);
    const [age,setAge] = useState(34);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在该代码片段中，我们分别定义了2个变量：name、age 以及他们对应的修改函数setName、setAge。&lt;/p&gt;
&lt;h2&gt;练习题&lt;/h2&gt;
&lt;p&gt;用useState实现一个计数器，默认为0，每次点击+1。&lt;/p&gt;
&lt;h5&gt;完整示例：&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;    import React, { useState } from &apos;react&apos;;
    
    function Component() {
    
      const [count, setCount] = useState(0);
    
      function clickHandler(){
        setCount(count+1);
      }
    
      return &amp;lt;div onClick={clickHandler}&amp;gt;
        {count}
      &amp;lt;/div&amp;gt;
    }
    
    export default Component;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请注意上述代码中，没有用到this，这就是函数组件中使用Hook的魅力之一，再也不用去关心烦人的this到底指向谁这个问题了。&lt;/p&gt;
&lt;p&gt;实际代码中，本人更加倾向于使用箭头函数来定义方法，所以上述 function clickHandler() 会写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    const clickHandler = () =&amp;gt; {
      setCount(count+1);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;至此，关于useState基础用法已经讲完。&lt;/p&gt;
</content:encoded></item><item><title>windows系统中VS code未识别nodejs cmdlet错误</title><link>https://blog.wemang.com/posts/go/faq/windows%E7%B3%BB%E7%BB%9F%E4%B8%ADvs-code%E6%9C%AA%E8%AF%86%E5%88%ABnodejs-cmdlet%E9%94%99%E8%AF%AF/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/faq/windows%E7%B3%BB%E7%BB%9F%E4%B8%ADvs-code%E6%9C%AA%E8%AF%86%E5%88%ABnodejs-cmdlet%E9%94%99%E8%AF%AF/</guid><pubDate>Tue, 16 May 2023 09:05:52 GMT</pubDate><content:encoded>&lt;h1&gt;报错&lt;/h1&gt;
&lt;p&gt;最近笔者在windows使用VS code开发的时候，遇到一个奇怪的问题，在调试代码的过程中，发现通过代码执行VS code的命令命令终端时，始终无法正常运行，一直报cmdlet错误，但是通过自己手动打开命令终端执行命令却一切正常。错误如下图所示：
&lt;img src=&quot;https://github.com/zhouzongyan/zhouzongyan.github.io/raw/image/20231013/image.1l92ck5nobi8.webp&quot; alt=&quot;错误&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;处理&lt;/h1&gt;
&lt;p&gt;经过查询资料发现，是由于windows权限问题，导致对应的脚本执行在代码打开的终端没有权限，直接就报错了，最开始由于编码问题看不到对应的错误信息，一直查询各种资料、代码断点，都没有发现问题，最后退出VS code，重新以管理员权限打开VS code再执行程序就解决了。
&lt;img src=&quot;https://github.com/zhouzongyan/zhouzongyan.github.io/raw/image/20231013/image.1venu2ofhzs0.webp&quot; alt=&quot;执行成功&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Go 配置 VS code 启动</title><link>https://blog.wemang.com/posts/go/faq/go%E9%85%8D%E7%BD%AEvscode%E5%90%AF%E5%8A%A8/</link><guid isPermaLink="true">https://blog.wemang.com/posts/go/faq/go%E9%85%8D%E7%BD%AEvscode%E5%90%AF%E5%8A%A8/</guid><pubDate>Mon, 15 May 2023 20:29:27 GMT</pubDate><content:encoded>&lt;p&gt;最近笔者在学习gokins过程中，最开始的时候，使用goland编译运行，但是后面由于免费试用天数用完就无法使用了，经过比较，还是决定使用VS code运行，但是VS code默认是不支持运行，需要自己手动配置&lt;/p&gt;
&lt;h2&gt;1、安装go插件&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/zhouzongyan.github.io/raw/image/20231013/image.2tobfm377v20.webp&quot; alt=&quot;go插件&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2、安装debug插件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;输入快捷键&lt;code&gt;Ctrl + Shift + P&lt;/code&gt;打开命令快捷输入框，输入&lt;code&gt;go install&lt;/code&gt;,选择下图的选项&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/zhouzongyan.github.io/raw/image/20231013/image.3it3pxblypi0.webp&quot; alt=&quot;debug插件&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;然后下一个选择框选择dlv 的debug组件（这边建议全选，避免后面使用到对应的工具又要再装一遍），点击OK就可以开始安装了，安装过程中由于是下载国外资源，会比较慢和失败，建议多试几次，如果实在不行的话，就使用手动安装的方式，安装方式请参考dlv的&lt;a href=&quot;https://github.com/derekparker/delve/tree/master/Documentation/installation&quot;&gt;github库指南&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zhouzongyan/zhouzongyan.github.io/raw/image/20231013/image.6q35wsamw2k0.webp&quot; alt=&quot;安装&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3、配置启动文件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ctrl+shift+p&lt;/code&gt; 输入 &lt;code&gt;Debug: Open launch.json&lt;/code&gt; 打开 &lt;code&gt;launch.json&lt;/code&gt; 文件，如果第一次打开,会新建一个配置文件，默认配置内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    &quot;version&quot;: &quot;0.2.0&quot;,
    &quot;configurations&quot;: [

		{
            &quot;name&quot;: &quot;Launch&quot;,
            &quot;type&quot;: &quot;go&quot;,
            &quot;request&quot;: &quot;launch&quot;,
            &quot;mode&quot;: &quot;auto&quot;,
            &quot;program&quot;: &quot;${fileDirname}&quot;,
            &quot;env&quot;: {},
            &quot;args&quot;: []
        }
	]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&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;name&lt;/td&gt;
&lt;td&gt;调试界面下拉选择项的名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;type&lt;/td&gt;
&lt;td&gt;设置为go无需改动，是 vs code 用于计算调试代码需要用哪个扩展&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mode&lt;/td&gt;
&lt;td&gt;可以设置为 auto, debug, remote, test, exec 中的一个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;program&lt;/td&gt;
&lt;td&gt;调试程序的路径（绝对路径）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;env&lt;/td&gt;
&lt;td&gt;调试时使用的环境变量。例如:&lt;code&gt;{ &quot;ENVNAME&quot;: &quot;ENVVALUE&quot; }      &lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;envFile&lt;/td&gt;
&lt;td&gt;包含环境变量文件的绝对路径，在 env 中设置的属性会覆盖 envFile 中的配置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;args&lt;/td&gt;
&lt;td&gt;传给正在调试程序命令行参数数组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;showLog&lt;/td&gt;
&lt;td&gt;布尔值，是否将调试信息输出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;logOutput&lt;/td&gt;
&lt;td&gt;配置调试输出的组件（debugger, gdbwire, lldbout, debuglineerr, rpc）,使用,分隔， showLog 设置为 true 时，此项配置生效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;buildFlags&lt;/td&gt;
&lt;td&gt;构建 go 程序时传给 go 编译器的标志，如果需要使用构建标记（e.g. go build -tags=whatever_tag）在参数 buildFlags 里写入 -tags=whatever_tag&quot; 即可，支持多标签，使用单引号将标签包围,例如： &quot;-tags=&apos;first_tag second_tag third_tag&apos;&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remotePath&lt;/td&gt;
&lt;td&gt;远程调试程序的绝对路径，当 mode 设置为 remote 时有效&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;其中对应的常用VS code常用公用变量如下所示：&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;tr&gt;
&lt;td&gt;------------------&lt;/td&gt;
&lt;td&gt;---------------------------------------------&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${workspaceFolder}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;调试 VS Code 打开工作空间的根目录下的所有文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${file}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;调试当前文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${fileDirname }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;调试当前文件所在目录下的所有文件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;具体的其他配置，请参考VS code 的&lt;a href=&quot;https://code.visualstudio.com/docs/editor/debugging#_launch-configurations&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item></channel></rss>