[{"content":"封面PID=126010542\n由于OpenWrt的dhcp服务默认是下发IPv6的dns给客户端的，导致AdguardHome的日志中会出现很多IPv4混杂IPv6的客户端，对于排查问题不是很友好遂决定禁用IPv6的dns下发，让所有客户端只通过IPv4 dns来查询解析。\n禁用v6 DNS服务 根据官方文档介绍，禁用调odhcpd的v6 dns：\n1 2 3 4 uci set dhcp.lan.dns_service=\u0026#34;0\u0026#34; uci set dhcp.lan.ra_dns=\u0026#34;0\u0026#34; uci commit dhcp service odhcpd restart 同时，修改dhcp下发的dns地址为自建的服务地址，我是直接在op上跑的，所以就是路由器本身：\n1 2 3 4 uci -q delete dhcp.lan.dhcp_option uci add_list dhcp.lan.dhcp_option=\u0026#34;6,192.168.1.1\u0026#34; uci commit dhcp service dnsmasq restart 这样重新连接网络，客户端就不会再拿到IPv6的dns啦。查询也都是走的v4，效果如图：\nDNS污染问题 去年手机换成了小米15，然后发现访问谷歌时不时会出现dns污染，通过查看服务日志发现v4的查询记录均是正常的，没发现返回了错误的ip。最后在v2ex的一贴讨论1中找到了线索，貌似是小米在系统中预设了几个公共DNS，导致有部分场景下DNS泄漏了。\n解决方法也比较简单，手动设置下DNS重定向就行了，由于我已经使用了AdguardHome代替了dnsmasq监听53端口，因此需要手动配置下防火墙转发：\n1 2 3 4 5 6 7 8 # V4 DNS Hijack uci add firewall redirect uci set firewall.@redirect[-1].target=\u0026#39;DNAT\u0026#39; uci set firewall.@redirect[-1].family=\u0026#39;ipv4\u0026#39; uci set firewall.@redirect[-1].src=\u0026#39;lan\u0026#39; uci set firewall.@redirect[-1].src_dport=\u0026#39;53\u0026#39; uci set firewall.@redirect[-1].name=\u0026#39;DNS\u0026#39; uci commit firewall 当然也不能忘了我们亲爱的v6 DNS由于我们没有提供v6服务给客户端，因此针对客户端的v6查询直接禁用即可：\n1 2 3 4 5 6 7 8 9 # V6 DNS Block uci add firewall rule uci set firewall.@rule[-1].src=\u0026#39;lan\u0026#39; uci set firewall.@rule[-1].dest=\u0026#39;*\u0026#39; uci set firewall.@rule[-1].family=\u0026#39;ipv6\u0026#39; uci set firewall.@rule[-1].dest_port=\u0026#39;53\u0026#39; uci set firewall.@rule[-1].target=\u0026#39;REJECT\u0026#39; uci set firewall.@rule[-1].name=\u0026#39;DNS6BLOCK\u0026#39; uci commit firewall 优先返回IPv4解析 问题 我在本地有使用jackett + flaresolverr + sonarr进行pt资源的抓取，自动订阅番剧。flaresolverr可以简单理解为一个代理工具，负责帮我们处理jackett抓取pt资源时遇到的人机校验，主要是cloudflare的。然而flaresolverr要求自身访问的请求环境必须和客户端（jackett）一致，否则可能出现获取cookie无效的情况。\n在我的设备环境下有个比较尴尬的情况，就是这两服务的容器分别使用了IPv4和IPv6的解析去获取数据，导致没法绕过人机验证。\n那有什么办法可以解决吗？直接禁用容器IPv6？这会误伤我一些只支持v6的pt站点，比如北邮人。以我的场景，更希望实现的是如果一个域名同时拥有v4和v6地址时，所有客户端只使用v4的解析，但又不影响纯v6域名的访问。\n问为什么优先使用v4？因为现在国外一些域名的v6路由的异常的逆天抽象。不信？しょうがないなあ，让你看看拷贝漫画cdn域名的路由吧：\n接下来介绍下DNS查询的逻辑。查询由客户端发起，并且由客户端指定查询的记录类型（比如A或AAAA），一般客户端会同时发起两个查询请求，分别查询v6和v4的解析记录，最后由客户端选择一个解析来访问服务（现代客户端一般会优先使用AAAA）。那目前就有两种解法，一是让客户端优先使用v4解析；二是尝试过滤查询结果。\n让客户端优先使用v4解析，大部分linux可以通过设置对应发行版的解析配置文件来解决，例如debain系的/etc/gai.conf文件2，但是不巧的是这两容器使用的系统为alpine，并不支持配置3，因此第一个方法可以排除。\n剩下一种方法便是过滤查询结果了，这里的简单思路是在客户端查询AAAA记录时，同时检查域名是否存在A记录，如果存在，则将AAAA的请求拦截并返回空给客户端，这样客户端拿不到AAAA记录，就只能使用A记录了。\n代码实现 这里用Go写了个简单的DNS服务器作过滤，实现这个效果：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 func HandleDNSRequest(writer dns.ResponseWriter, req *dns.Msg) { c := new(dns.Client) c.Net = \u0026#34;udp\u0026#34; var upstreamDNS = *UpstreamAddress resp, _, err := c.Exchange(req, upstreamDNS) if err != nil { log.Printf(\u0026#34;Query upstream DNS error: %v\\n\u0026#34;, err) return } defer writer.WriteMsg(resp) // 非标准DNS查询，或结果为空，直接返回 if len(req.Question) != 1 || len(resp.Answer) == 0 { return } // 非AAAA查询，直接返回 if req.Question[0].Qtype != dns.TypeAAAA { return } // 尝试获取域名A记录 m := new(dns.Msg) m.SetQuestion(req.Question[0].Name, dns.TypeA) r, _, err := c.Exchange(m, upstreamDNS) if err != nil { log.Printf(\u0026#34;Query upstream DNS error: %v\\n\u0026#34;, err) return } if len(r.Answer) == 0 { return } for _, rr := range r.Answer { if rr.Header().Rrtype == dns.TypeA { // 存在A记录，则将AAAA响应清空 resp.Answer = make([]dns.RR, 0) return } } } 测试下：\n1 2 3 4 5 6 7 $ nslookup -port=5367 www.google.com 127.0.0.1 Server: 127.0.0.1 Address: 127.0.0.1#5367 Non-authoritative answer: Name: www.google.com Address: 142.250.76.132 嗯~看起来效果不错，接下来再加些缓存优化，减少多次DNS查询带来的耗时：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func HandleDNSRequest(writer dns.ResponseWriter, req *dns.Msg) { ... if C != nil { // 先查看缓存是否已有记录 if hasA, exist := C.Get(req.Question[0].Name); exist { // 是否存在A记录 if hasA.(bool) { resp.Answer = make([]dns.RR, 0) } return } } ... for _, rr := range r.Answer { if rr.Header().Rrtype == dns.TypeA { // 设置A缓存记录存在 setARecordCache(req.Question[0].Name, true) resp.Answer = make([]dns.RR, 0) return } else { // 设置A缓存记录不存在 setARecordCache(req.Question[0].Name, false) } } } 完美搞定完整代码可以去看我GitHub的仓库dns-filter。\nMIUI 通过硬编码内置了 DNS？\u0026#160;\u0026#x21a9;\u0026#xfe0e;\ngai.conf(5) — Linux manual page\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nHow to resolve IPv4 first on Alpine Linux?\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-06-11T07:47:42Z","image":"https://blog.jarao.work/articles/2025/06/v6-dns/cover_hu_88b226819c13c584.jpg","permalink":"https://blog.jarao.work/articles/2025/06/v6-dns/","title":"在OpenWrt上遇到的v6 DNS相关问题"},{"content":"封面PID=112536466\n前段时间为了排查路由器的一些服务问题，去翻系统日志，发现自己的路由器因为有在公网上被扫了几百次（好家伙，我都换端口了还能扫😅），整个系统日志全是各种ssh连接失败的报错。虽然禁用了密码登录，但是整个日志被刷的太乱，导致正常运维都成问题，属实是伤害不高侮辱性极强了。于是为了保证我的路由器不被这些脚本小子搭讪，开始研究如何使用Fail2Ban来自动拉黑这些扫描的脚本机。\n安装 这块没什么好说的，不管是官方源还是目前国内普及的lean lede源都有fail2ban的包，直接opkg就行\n1 opkg install fail2ban 安装好后在终端中输入which fail2ban-client，验证下是否安装成功，正常的话会输出对应的执行文件路径。\n配置 默认配置文件目录在/etc/fail2ban。Fail2Ban需要配置两个地方，一个是过滤规则，或者说是正则匹配逻辑；一个是配置具体封禁策略，包含hit次数，日志位置等。\n过滤规则 /etc/fail2ban/filter.d下是项目自带的默认过滤规则，而官方本身OpenWrt的dropbaer规则不能直接使用，不知道是不是因为迭代没跟上还是怎么的，dropbear的日志匹配逻辑对不上，因此需要我们自己修改过滤规则。下面给出我自己配置的规则供参考，把官方的dropbear.conf改了即可：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 # The standard Dropbear output doesn\u0026#39;t provide enough information to # ban all types of attack. The Dropbear patch adds IP address # information to the \u0026#39;exit before auth\u0026#39; message which is always # produced for any form of non-successful login. It is that message # which this file matches. # # More information: http://bugs.debian.org/546913 [INCLUDES] # Read common prefixes. If any customizations available -- read them from # common.local before = common.conf [Definition] _daemon = dropbear datepattern = ^%%a %%b %%d %%H:%%M:%%S %%Y prefregex = ^%(__prefix_line)s\u0026lt;F-CONTENT\u0026gt;(?:[Ll]ogin|[Bb]ad|[Ee]xit).+\u0026lt;/F-CONTENT\u0026gt;$ failregex = ^[Ee]xit before auth from \u0026lt;\u0026lt;HOST\u0026gt;:\\d+\u0026gt;:\\s.*$ ignoreregex = # DEV Notes: # # The first two regexs here match the unmodified dropbear messages. It isn\u0026#39;t # possible to match the source of the \u0026#39;exit before auth\u0026#39; messages from dropbear # as they don\u0026#39;t include the \u0026#34;from \u0026lt;HOST\u0026gt;\u0026#34; bit. # # The second last failregex line we need to match with the modified dropbear. # # For the second regex the following apply: # # http://www.netmite.com/android/mydroid/external/dropbear/svr-authpam.c # http://svn.dd-wrt.com/changeset/16642#file64 # # http://svn.dd-wrt.com/changeset/16642/src/router/dropbear/svr-authpasswd.c # # Author: Francis Russell # Zak B. Elep 接下来，我们需要把系统日志重定向到系统文件中，使用以下命令操作：\n1 2 3 4 mkdir -p /tmp/log touch /tmp/log/system.log uci set system.@system[0].log_file=\u0026#39;/tmp/log/system.log\u0026#39; uci commit 这套过滤规则可以过滤出登录失败的主机ip。fail2ban也有专门的命令可以测试配置有效性，可以使用fail2ban-regex /tmp/log/system.log /etc/fail2ban/filter.d/dropbear.conf命令测试过滤匹配是否正确。\n封禁策略 随后我们还需要配置封禁策略，这个策略一般是放在/etc/fail2ban/jail.d目录下，我们可以在这个目录下面新建一个策略名为dropbear.local的文件，在其中配置命中策略：\n1 2 3 4 5 6 7 8 9 10 11 [dropbear] enabled = true filter = dropbear # port为你对应的ssh端口 action = iptables[port=22, protocol=tcp] # 上一步创建的日志文件路径 logpath = /tmp/log/system.log maxretry = 3 bantime = 604800 findtime = 86400 这个封禁策略会将在一天内ssh登录失败3次的ip封禁一个星期。\n最后，我们使用命令fail2ban-client start启动Fail2Ban。\n效果 使用命令fail2ban-client status dropbear，查看当前dropbear规则的封禁情况\n可以看到，这封禁ip的数量还真不少，用ipip查了下，国内国外的都有。\n进阶 luci登录封禁 既然我们都把dropbear配了登录封禁，那么就顺道给web登录页面也配一个吧😉\n具体步骤和dropbear一样，由于我用的是nginx作为luci的服务器，而且修改了日志格式，因此以下配置仅供参考。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [INCLUDES] # Read common prefixes.If any customizations available -- read them from # common.local before = common.conf [Definition] _daemon = luci datepattern = ^\\[%%d/%%b/%%Y:%%H:%%M:%%S \\+0800\\] prefregex = ^.*POST.*\u0026lt;F-CONTENT\u0026gt;(?:403).+\u0026lt;/F-CONTENT\u0026gt;$ failregex = ^403.*from \u0026lt;HOST\u0026gt;$ ignoreregex = ^403.*from 127.0.0.1$ 其中过滤规则的ignore是因为luci在第一次进入主页时会触发本地反代403，因此需要忽略。日期格式匹配可能需要自行根据自己的nginx日志进行调整。\n最后加上封禁策略：\n1 2 3 4 5 6 7 8 9 10 # luci web [luci] enabled = true filter = luci action = iptables[port=https, protocol=tcp] logpath = /tmp/log/nginx/access.log maxretry = 5 bantime = 604800 findtime = 1800 重启Fail2Ban，收工！\n","date":"2023-10-19T08:07:00.331Z","image":"https://blog.jarao.work/articles/2023/10/fail2ban/cover_hu_69fe5c2ff435ee06.png","permalink":"https://blog.jarao.work/articles/2023/10/fail2ban/","title":"在OpenWrt上配置Fail2Ban"},{"content":"前段时间在公司提交代码的时候，看到仓库密密麻麻格式不一的commit记录有点头疼🤣，去问了下同事发现这块好像大家普遍都不注重，因此专门做了下关于约定式提交的调查。\n为什么要规范commit 代码的Git提交是每个项目必经且十分频繁的操作，一个大型的、长期的项目如果在没有commit规范的情况下进行迭代，在经由不同的时期、不同的团队迭代后，commit记录会出现五花八门的格式，一是每个人的信息格式不一致会导致查阅者获取关键信息的效率低，二是时间久了就会出现因commit消息不清晰导致无法有效追溯变更记录。\n这种情况在开源社区更是常见，因此基本上所有大型的开源项目为了保证信息规整有效，都会设立对应项目的commit规范。这不仅对于获取commit有效信息有很大的帮助，同时会让一个开发者去注意思考每次commit的功能、特性划分的合理性，如果配合一定的工具，还能够简便的生成规范的release日志。\n其实说白了主要目的就是：\n规范代码commit的信息，提高获取关键信息的效率，减少因团队更迭后造成的信息出入 通过git工具，快速生成标准release日志 获得共识，对齐业界（好歹别太low对吧🤪） 业界调研 当前我所在的公司没有commit的明确规范标准，而业界当前大部分的commit规范是基于「Angular Commit Guidelines」，这是一个js的commit规范，但是由于其定义的严谨性和通用性而被大部分项目采纳。\n实际上这块还涉及一个版本号管理规范「语义化版本 2.0.0 | Semantic Versioning」，起初也是前端为了管理包版本设计的）\n项目 Blocky React 高德地图 Bootstrap nginx electron openwrt 公司（社区） Google Meta 阿里 Twitter nginx electron openwrt/lede commit规范 约定式提交（基于 Angular Commit Guidelines） Angular Commit Guidelines 如何规范你的Git commit？（基于Angular Commit Guidelines） tbaggery - A Note About Git Commit Messages Contributing Changes (nginx.org) 约定式提交（基于 Angular Commit Guidelines） [OpenWrt Wiki] Submitting patches 共性 1. 以commit类型为开头，例如feat，fix，build\n2. 类型后紧跟描述信息\n3. subject或description长度不是都限制，但一般不宜超过50~100，力求言简意赅\n4. body、footer可选，可以没有\n5. 不在末尾增加句号\n6. 会根据项目特性进行适当调整 Angular Commit Guidelines文档介绍中提及了「Google Style Guides | styleguide」，其部分设计思路是遵循该文档。\n约定式提交使用介绍 特点 约定式提交则在保留了Angular规范的基础上，进行了部分调整和优化，例如明确允许scope的内容可选填（实际上阿里的规范也有这个优化，业内社区也普遍没有要求scope内容）；在具有破坏性的commit上使用「!」标记（对标SemVer强调破坏性提交的特点）等，并且谷歌、electorn等公司、项目也有使用，也算是受到业界认可的规范标准。\n当然还有最重要的一点是，他有专门的社区维护，文档十分的完善，基本我们可以做到开箱即用，并且社区也提供了一些自动化工具。\n具体规范我就不过多介绍（因为官方文档真的很详细了），以下摘抄一部分官方文档，可以感受下。\n官方文档 概述 约定式提交规范是一种基于提交信息的轻量级约定。 它提供了一组简单规则来创建清晰的提交历史； 这更有利于编写自动化工具。 通过在提交信息中描述功能、修复和破坏性变更， 使这种惯例与 SemVer 相互对应。\n提交说明的结构如下所示：\n1 2 3 4 5 \u0026lt;类型\u0026gt;[可选 范围]: \u0026lt;描述\u0026gt; [可选 正文] [可选 脚注] 提交说明包含了下面的结构化元素，以向类库使用者表明其意图：\nfix: 类型 为 fix 的提交表示在代码库中修复了一个 bug（这和语义化版本中的 PATCH 相对应）。 feat: 类型 为 feat 的提交表示在代码库中新增了一个功能（这和语义化版本中的 MINOR 相对应）。 BREAKING CHANGE: 在脚注中包含 BREAKING CHANGE: 或 \u0026lt;类型\u0026gt;(范围) 后面有一个 ! 的提交，表示引入了破坏性 API 变更（这和语义化版本中的 MAJOR 相对应）。 破坏性变更可以是任意 类型 提交的一部分。 除 fix: 和 feat: 之外，也可以使用其它提交 类型 ，例如 @commitlint/config-conventional（基于 Angular 约定）中推荐的 build:、chore:、 ci:、docs:、style:、refactor:、perf:、test:，等等。 脚注中除了 BREAKING CHANGE: ，其它条目应该采用类似 git trailer format 这样的惯例。 其它提交类型在约定式提交规范中并没有强制限制，并且在语义化版本中没有隐式影响（除非它们包含 BREAKING CHANGE）。 可以为提交类型添加一个围在圆括号内的范围，以为其提供额外的上下文信息。例如 feat(parser): adds ability to parse arrays.。\n示例 包含了描述并且脚注中有破坏性变更的提交说明\n1 2 3 feat: allow provided config object to extend other configs BREAKING CHANGE: `extends` key in config file is now used for extending other config files 包含了 ! 字符以提醒注意破坏性变更的提交说明\n1 feat!: send an email to the customer when a product is shipped 包含了范围和破坏性变更 ! 的提交說明\n1 feat(api)!: send an email to the customer when a product is shipped 包含了 ! 和 BREAKING CHANGE 脚注的提交说明\n1 2 3 chore!: drop support for Node 6 BREAKING CHANGE: use JavaScript features not available in Node 6. 不包含正文的提交说明\n1 docs: correct spelling of CHANGELOG 包含范围的提交说明\n1 feat(lang): add polish language 包含多行正文和多行脚注的提交说明\n1 2 3 4 5 6 7 8 9 10 fix: prevent racing of requests Introduce a request id and a reference to latest request. Dismiss incoming responses other than from latest request. Remove timeouts which were used to mitigate the racing issue but are obsolete now. Reviewed-by: Z Refs: #123 为什么使用约定式提交 自动化生成 CHANGELOG。 基于提交的类型，自动决定语义化的版本变更。 向同事、公众与其他利益关系者传达变化的性质。 触发构建和部署流程。 让人们探索一个更加结构化的提交历史，以便降低对你的项目做出贡献的难度。 根据commit生成release日志 既然commit有了固定的格式，那我们自然可以根据这个格式来生成标准的日志啦\n由于公司项目不是前端项目，也不是新项目，实在没法使用社区里自带的日志生成工具，于是就自己写了段shell支持🫠\n1 2 3 4 5 git log --pretty=format:\u0026#34;%s https://url/commit/%H\u0026#34; | \\ sed \u0026#39;/\u0026#39;$(git show-ref --tags | grep \u0026#34;project/v\u0026#34; | awk \u0026#39;END{print}\u0026#39; | awk \u0026#39;{print $1}\u0026#39;)\u0026#39;/,$d\u0026#39; | \\ grep -E \u0026#34;^(feat|fix|refactor|chore): \u0026#34; | \\ grep -v Merge | awk -F\u0026#39;: \u0026#39; \u0026#39;{print \u0026#34;[\u0026#34;$1\u0026#34;]\u0026#34;,$2}\u0026#39; | \\ sed \u0026#39;s:\\[feat\\]:\\[feature\\]:g\u0026#39; | sed \u0026#39;s:\\[fix\\]:\\[bugfix\\]:g\u0026#39; 已经分享给公司的同事，希望今后团队commit能有改善吧🤣\n相关文档：\n约定式提交 Angular Commit Guidelines Google Style Guides | styleguide 语义化版本 2.0.0 | Semantic Versioning 如何规范你的Git commit？（基于Angular Commit Guidelines） [OpenWrt Wiki] Submitting patches tbaggery - A Note About Git Commit Messages Contributing Changes (nginx.org) ","date":"2023-07-27T11:12:44+08:00","image":"https://blog.jarao.work/articles/2023/07/conventional-commits/cover_hu_93f4e5edac18aea6.png","permalink":"https://blog.jarao.work/articles/2023/07/conventional-commits/","title":"约定式提交 Conventional Commits"},{"content":"封面PID=97289746\n问题 这两天在自己服务器上安东西的时候突然发现CentOS 8的源更新报错了。\n一下给我整懵了，记得上次update还是上次，没遇到这问题啊。第一反应是去看看源文件是不是出了什么岔子。\n然而实际上源文件的源url也是正常的，这就奇怪了，同时也检查了下域名解析的dns，一切正常。这个时候基本可以排除本地问题的可能性了，那还有可能就是官方的源出了什么问题了。\n原因 之后去官网一看才想起来，CentOS 8之前已经宣布在2021年年底放弃维护了，因此他的官方源也已经失效了\u0026hellip;\n原文链接\nCentOS Linux 8 will reach End Of Life (EOL) on December 31st, 2021.\nThis content will be removed from our mirrors, and moved to vault.centos.org where it will be archived permanently, since we will not be able to provide updates to the content after the EOL date.\n解决方法 根据通知原文描述，我们可以使用官方的归档源，官方归档域名是vault.centos.org，只需要把源文件的域名换成归档域名就行了。\n1 2 3 # 在shell中使用sed直接修改所有源的域名 sed -i \u0026#39;s/mirrorlist/#mirrorlist/g\u0026#39; /etc/yum.repos.d/CentOS-* sed -i \u0026#39;s|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g\u0026#39; /etc/yum.repos.d/CentOS-* 但是官方论坛的版主还是建议尽快更换系统分支，因为更换归档源并不能解决根本问题，CentOS8后续不再维护才是真正的问题。\nNo. Problem NOT solved. That just switches the repos to vault.centos.org but CentOS 8 is dead and will not receive any more updates. Your solution does not handle that.\nYou need to switch to a different distro, not just point to vault which is already stale and out of date.\n顺带一提，CentOS 7将在2024年结束维护，以后再也没有免费的CentOS给我们用了，CentOS也要成时泪了。\n","date":"2022-04-06T02:02:27+08:00","image":"https://blog.jarao.work/articles/2022/04/centos-8-eol/cover_hu_b2c691d644d46c2e.jpg","permalink":"https://blog.jarao.work/articles/2022/04/centos-8-eol/","title":"CentOS 8 EOL导致更新源失败"},{"content":"上班上到一半，收到手办到家的快递信息，之前在Hpoi买的阿米娅景品到了，整得我上班心猿意马，急着下班回家拆箱\n未拆前盒装 拆包装前先拍一个盒子，看起来包装很不错。\n拆封 拆开以后拿手上发现用料还是挺扎实的，就是尺寸对比之前妇幼的黑兔要小很多（废话都是泡面压了)。\n感觉比起正面，书包的细节要更多一些\n黑兔对比 总体上来说还行吧，但脸得吃特定角度才能拍得好看，不然总有点邪神的感觉。\n","date":"2022-03-25T00:41:07+08:00","image":"https://blog.jarao.work/articles/2022/03/amiya-get/cover_hu_100f8712669db3cd.jpg","permalink":"https://blog.jarao.work/articles/2022/03/amiya-get/","title":"Amiya Get!"},{"content":"封面PID=98135423\nGo自从1.18开始正式支持了泛型，官方称其为Go开源发布之后最大的一次变更。\n在出现泛型之前，Go传递不定参数的通用方法是传递接口，这次支持泛型无疑是对Go编码影响巨大的升级。\n本文针对学习过程中的一些要点做初步记录。\n泛型 Go支持了泛型，为其带来了三个新的特性：\nType parameters for function and types. Defining interface types as sets of types, including types that don’t have methods. Type inference, which permits omitting type arguments in many cases when calling a function. 简单来说，就是支持了函数、type关键字的类型参数，使用无方法接口实现类型集，函数调用泛型推理省略类型参数。\n以下是参照Go的官方例子做的示例：\n1 2 3 4 5 6 7 8 9 10 11 // 无方法接口实现类型集 type Number interface { int | float64 } // 函数类型参数 func GMin[T Number](x, y T) T { if x \u0026lt; y { return x } return y } Hit:\n类型参数用[]来进行定义，泛型标识符T。 Go的编译器会在替换泛型标识符后检查实际类型是否满足函数的类型约束条件。 1 2 3 4 // 这一步编译器会进行函数实例化，如果类型不满足函数内部约束条件（本例里是运算符 \u0026#39;\u0026lt;\u0026#39; ），则会报错 fmin := GMin[float64] // float64在这就是实际指定的类型 m := fmin(2.71, 3.14) // 实例化成功后可正常调用 fmt.Println(m) // 2.71 其中Go在进行编译时会进行类型推算（type inference），因此你也可以将你的函数写成：\n1 2 m := GMin(2.71, 3.14) // 不需要手动指定T的真实类型，Go自己推算 fmt.Println(m) // 2.71 以上是一个简单的Go泛型函数调用，其中我们可以看到Go使用了接口关键字interface关键字来定义泛型的约束类型。 接下来我们就说说为啥Go是用interface来定义泛型的约束类型集。\n泛型约束集 接口定义了一系列方法，实现了这些方法的type可以表现成type-\u0026gt;接口的一种映射关系；从另一个角度上来说，接口同样定义了一个实现了这些方法的type的集合，这个集合中的type都实现了接口的方法。因此我们反过来可以获得接口-\u0026gt;type的映射关系\n官方博客里的这张图片里展示了一个接口在被多个type实现时所具有的集合关系，其中每一个圈都是一个接口的方法集。可以看到，typeP、Q、R在实现了interface后，会在中心区域有一个“方法交集”。这个交集反过来定义了下图的类型集。\n一般的接口是方法的接口，这里泛型的接口可以理解为类型的接口。\nGo的泛型还支持指定某个Underlying Type，例如：\n1 2 3 type Number interface { ~int | float64 } 上述代码中，~int表示所有以int为Underlying Type的类型。\n泛型的Core Type 需要注意的是Go的在使用内置函数的时候需要确定类型的Core Type，比如使用make、range等操作。如果你的泛型interface没有Core Type，你会看到类似如下报错\n1 2 3 4 5 6 7 8 9 10 type NumberSlice interface { []float64 | []int | []string } func test[T NumberSlice](num T) { // cannot range over num (variable of type T constrained by NumberSlice) (T has no core type) for _, v := range num { // do something } } 这是因为Go定义类型接口的Core Type只在两种情况存在：\n接口类型集存在统一的Underlying Type 接口类型集存在统一的管道，管道的类型、方向需一致 像上面的代码，NumberSlice由于不存在相同的Underlying Type，因此不具有Core Type，不能使用range关键字进行操作。不过我们可以通过调整泛型来实现相同的功能：\n1 2 3 4 5 6 7 8 9 type Number interface { float64 | int } func test[T Number](num []T) { for _, v := range num { // do something } } 这样就可以正常的实现遍历泛型slice了。\n相关文档：\nThe Go Programming Language Specification: Core Type An Introduction To Generics ","date":"2022-03-24T21:04:21+08:00","image":"https://blog.jarao.work/articles/2022/03/learn-go-generic/cover_hu_3d8e4431fb66dc40.png","permalink":"https://blog.jarao.work/articles/2022/03/learn-go-generic/","title":"Go泛型学习笔记"},{"content":"封面PID=96033443\nHello World 这是毕业工作以后新建的第一个blog，第一篇文章，Hello World，做下纪念 :) 在原主题的基础上做了些自己喜欢的风格修改，但是由于时间不够（前端苦手🤣）只能先粗略改下。 建议本站点使用暗色模式访问。 ","date":"2022-03-19T23:35:34+08:00","image":"https://blog.jarao.work/articles/2022/03/hello-world/cover_hu_277ae5feafc7805b.jpg","permalink":"https://blog.jarao.work/articles/2022/03/hello-world/","title":"建站纪念"}]