[{"content":" 操作系统简介 了解操作系统：操作计算机就是操作硬件（CPU，内存，硬盘等），而各类硬件只能识别机器语言。为了方便操作硬件，我们在硬件的基础上安装的各种操作系统。\n操作系统的分类：\n桌面操作系统 Windows：用户群体大 macOS：适合开发人员 Linux：应用软件少 服务器操作系统 Linux：安全，稳定，免费，占有率高 Windows Server：付费，只有率低 嵌入式操作系统 Linux 移动设备操作系统 iOS Android（基于Linux） Linux的内核版和发行版 Linux的内核版本只有一个，而发行版都是基于内核版本开发的，发行版包含了各种应用软件。\n常见的发行版有：Ubuntu（乌班图），Redhat，Debian，CentOS等。\nLinux操作系统目录结构 【大致了解即可：】\n/：根目录，一般根目录只存放目录，在Linux下有且只有一个根目录，所有的东西都是从这里开始的。 /home：系统存放用户的家目录，新增用户账号时，用户的家目录都存放在此目录下。用用户名作为目录名存放。 ~：表示当前用户的家目录。 /bin, /usr/bin: 可执行二进制文件的目录，如常用的命令ls,tar,mv,cat等。 /sbin: 存放系统管理员相关的可执行二进制文件。存放系统二进制文件。 /usr/bin(user commands for applications)后期安装的一些软件。 /usr/sbin(super user commands for applications)超级用户的一些管理程序。如useradd等。 /boot: 放置Linux系统启动时用到的一些文件，如Linux的内核文件：/boot/vmlinuz,系统引导管理器：/boot/grub /dev: 存放linux系统下的设备文件，访问该目录下的某个文件，相当于访问某个设备，常用的是挂载光驱mount /dev/cdrom /mnt/cdrom。 /etc: 系统配置文件存放的目录，不建议在此目录下存放可执行文件，重要的配置文件如： /etc/inittab /etc/fstab /etc/init.d /etc/X11 /etc/sysconfig /etc/xinetd.d /lib, /usr/lib, /usr/local/lib: 系统使用的函数的目录，程序在执行的过程中，需要调用一些额外的参数时需要函数库的协助。 /lost+fount: 系统异常产生错误时，会将一些遗失的片段放置于此目录下。 /mnt, /media: 光盘默认挂载点，通常光盘挂载于/mnt/cdrom下，也不一定，也可以选择任意位置进行挂载。挂载目录。 /opt：给主机额外安装软件所的存放目录。 /proc: 此目录的数据都在内存中，如系统核心，外部设备，网络状态，由于数据都存放于内存中，所以不占用磁盘空间，都是些比较重要的文件。 如/proc/cpuinfo, /proc/interrupts, /proc/dma, /proc/ioports, /proc/net/*等。 /sys: 类似于/proc, 也是一个伪文件系统，提供关于系统信息的虚拟目录。 /root: 系统管理员root的家目录。 /srv: 存储系统服务数据的地方。 /tmp: 存储临时文件的地方。 /usr: 存储用户可用的程序和只读的文件。 /var: 存储系统运行时产生的可变数据。如日志文件、邮件、系统数据库等。 /run: 存储系统运行时产生的临时文件。如运行中的进程和服务的状态信息。 ps：系统目录了解，只需要知道常见的目录进行\nLinux命令基础* 终端命令格式 不仅是在终端中运行的命令，命令格式大多数情况下都是通用的。\n终端命令格式：\n1 command [-options] [parameter] command: 命令名，相应功能的命令名，如ls，mv。 []: 表示可选的。 options: 选项，对命令进行控制，可以省略。 可以多开选项组合使用，只有一个减号。称之为短选项。 还有长选项，两个减号。对应命令就比较长。 parameter：传给命令的参数，可以是零个，一个或者多个。 放大或缩小终端字体 放大终端字体：ctrl-shift-=：相当于ctrl-+。 注意：部分终端不需要shift。 缩写终端字体：ctrl--。 自动补全 自动补全: 在敲出文件/目录/命令/的前几个字母之后，按下tab键，如果没有歧义，会自动补全，有歧义会显示歧义的内容。 曾经使用过的命令：使用上下键可以自由切换在终端中使用过的命令。 ctrl-c: 可以退出使用。多数情况下，退出都可以使用ctrl-c。 查看命令帮助信息 command --help: 显示命令command的帮助信息。\n短选项-h只有部分命令可用，一般推荐使用长选项–help。 man command：查看命令command的使用手册(manual:使用手册)：\n是由Linux提供的手册，包含了大部分命令，选项的详细说明。\n常用操作键：\n空格键：显示手册页的下一屏\nenter：滚动手册页的下一行\nb：回滚一屏\nf：前滚一屏\nq：退出\n/word: 搜索word字符串\nh：查看man命令帮助信息。\nps：我们要知道上面两种查看命令帮助信息的方式，对于查询结果看不懂没关系，我们只需要学习常用命令及常用选项的使用即可，学会之后可以尝试阅读。对于不知道的命令、选项可以借助网络搜索。\n学习技巧 学习技巧：\n不需要死记硬背，对于常用命令，用得多了，自然就记住了。 不要尝试一次学会所有的命令，有些命令是非常不常用的，临时遇到，临时查询就可以了。 Linux下文件和目录的特点 Linux文件或目录名称最长可以有256个字符。 以.开头的文件或目录是自动隐藏的，需要使用-a参数才能显示。 .表示当前目录。 ..表示上一级目录。 计算机中文件大小的表示方式 单位 英文 含义 字节 B(byte) 在计算机中作为一个数字单元，一般为8位二进制数 千 K(Kibibyte) 1KiB=1024B：千字节 兆 M(Mebibyte) 1MiB=1024KiB：百万字节 千兆 G(Gigabyte) 1GiB=1024MiB：十亿字节，千兆字节 太 T(Terabyte) 1TiB=1024GiB：万亿字节，太字节 注意：\n计算机中不同单位之间转换是8的倍数。如：100Mib = 12.5 MiB。也就是我们说的100兆，实际速率有12.5兆字节。 相同单位之间，计算机中是1024倍，现实生活中是1000倍。 为了区分，所以计算机中单位通常多个i。 所以我们购买的硬盘容量在计算机中往往偏小。 ps：更多内容请网络搜索。\n相对路径和绝对路径 相对路径：在输入路径时，不是以/或者~开头的路径，表示相对当前目录所在的目录位置。如: ../python表示上一级目录中的python文件或目录。 绝对路径：在输入路径时，最前面是以/或者~开头的路径，表示从根目录或者家目录开始的具体目录位置。如：/home/centos/python或者~/python。 通配符的使用 【了解】\n通配符 含义 * 表示任意个字符 ? 表示任意一个字符，只有一个 [] 表示可以匹配字符组中的任意一个 [abc] 匹配a，b，c中的任意一个 [a-f] 匹配a到f范围内的任意一个 网卡和IP地址 网卡：网卡是一个专门负责网络通讯的硬件设备。 IP地址是设置在网卡上的地址信息。 理解：电脑就是手机，网卡就是SIM卡，IP地址就是电话号码。 有了IP地址电脑才能连接到其他电脑，也就是连接互联网。 一台计算机中可以会有一个物理网卡和多个虚拟网卡，在Linux中物理网卡的名字通常以ensXX表示。 127.0.0.1被称之为本地回环或者回环地址，一般用来测试本机网卡是否正常。 网络上的机器都有唯一确定的IP地址，我们给目标IP地址发送一个数据包，对方就要返回一个数据包，根据返回的数据包以及时间，我们可以确定目标主机的存在。 在Linux中，想要终止一个终端程序的执行，绝大多数都可以ctrl+c。 SSH基础 在Linux中SSH是非常常用的工具，通过ssh客户端我们可以远程连接到运行了ssh服务器（sshd）的机器上。 SSH软件架构是客户端、服务端模式。客户端就是ssh，服务端是sshd。 ssh客户端是一种使用secure shell（ssh）协议连接到远程计算机的软件程序。登录远程服务器。 ssh是目前可靠，专为远程登录会话和其他网络服务提供安全性的协议。加密通信。 通过ssh协议：数据传输是加密的，可以防止信息泄露，数据传输是压缩的，可以提高传输速度。 利用ssh协议，可以有效防止远程管理过程中的信息泄露。 通过ssh协议，可以对所有传输的数据进行加密，也能防止DNS欺骗和IP欺骗。 ssh的另一项优点是传输的数据可以是经过压缩的，所以可以加快传输的速度。 SSH的安装 对于不同的Linux开发板有的安装了ssh和sshd，而有的没有。没有的就需要自己安装。\n注意：是服务器安装服务端sshd，客户端安装ssh。连接那台服务器就需要安装sshd，用那台电脑连接就需要安装ssh客户端。 查看sshd状态：service ssh status，如果提示ssh：unrecognized service，说明没有安装服务端，则需要安装sshd。\nsshd服务端的安装：sudo apt install openssh-server 可以再次输入service ssh status查看是否安装成功。 打开sshd服务：service ssh start 终止sshd服务：service ssh stop 重启sshd服务：service ssh restart 开机自启sshd服务：sudo systemctl enable ssh 查看是否安装ssh：whereis ssh，如果提示ssh：，说明没有安装ssh客户端。\nssh客户端的安装：sudo apt install openssh-client 登录远程服务器：ssh username@hostname 注意：\n以上操作系统是Ubuntu、Debian。如果是Centos、RedHat，安装的ssh客户端是openssh-clients，服务端的服务名是sshd。其他的没有区别。 Windows上如果没有安装ssh客户端，可以打开搜索添加可选功能，安装ssh客户端。它是系统自带的，也可以安装服务端sshd。 macOS也自带ssh客户端。也自带服务端，不过服务端默认是关闭的，需要手动开启。在共享设置中开启。 推荐文章：SSH Introduction。\nPS：更多内容请自行网络搜索。\n域名基础 域名（Domain Name）是互联网中用于标识一个网站或服务的友好名称。它提供了一种便捷的方式，让用户可以通过简单的字符序列（如www.example.com）而不是难记的IP地址来访问网站。是IP地址的别名，方便用户记忆。以下是关于域名的详细介绍：\n域名的基本概念 域名：是互联网上用于标识一个网站、服务器或网络资源的名字。一个域名由一串字母和数字组成，并按照层级结构排列。 IP地址：域名的背后是一个或多个IP地址，它们用于在网络中标识具体的计算机或服务器。**域名系统（DNS）**负责将域名解析为相应的IP地址。 URL（统一资源定位符）：URL是访问特定资源（如网页、文件等）的完整路径，它包含域名、协议（如HTTP/HTTPS）、路径等信息。例如，https://www.example.com/index.html。 域名的层级结构 域名采用分层结构，从右到左依次为顶级域名（TLD）、二级域名（SLD）以及子域名等：\n通用顶级域名（gTLD）：如 .com、.net、.org。 国家或地区顶级域名（ccTLD）：如 .cn（中国）、.uk（英国）、.jp（日本）。 新顶级域名（nTLD）：如 .xyz、.app、.tech。 二级域名（SLD）Second-level domain：它位于TLD的左侧，通常由用户自行选择或注册。例如，在 www.example.com 中，example 就是二级域名。www是它的子域名。 子域名（Subdomain）：子域名是二级域名之下的扩展部分。例如，在 blog.example.com 中，blog 就是 example.com 的一个子域名。 域名的分类 【仅供参考】\n按层级分类 顶级域名（Top-Level Domain, TLD）：这是域名结构中最右边的部分，通常分为以下几类：\n通用顶级域名（Generic Top-Level Domains, gTLDs）：这些域名类型不受地域限制，适用于全球范围。常见的有： .com：最常用的顶级域名，适用于商业机构和个人。 .net：最初为网络服务提供商保留，但现已广泛使用。 .org：主要用于非营利组织和非政府组织。 .info：用于信息网站。 .biz：用于商业网站。 .name：用于个人或家庭。 .gov：仅限政府机构使用（通常为美国政府）。 .edu：仅限教育机构使用（主要为美国的教育机构）。 国家或地区顶级域名（Country Code Top-Level Domains, ccTLDs）：这些域名专门为各个国家或地区分配，通常由两个字母组成。例如： .cn：中国 .uk：英国 .de：德国 .jp：日本 .fr：法国 .us：美国 新通用顶级域名（New Generic Top-Level Domains, nTLDs）：这些域名是近年来ICANN推出的新类别，提供更多细分的选择，如： .app：应用程序相关的网站。 .tech：技术或科技领域的网站。 .store：在线商店。 .xyz：通用性强，适用于各种类型的网站。 .online：在线服务或互联网相关内容。 .club：适用于俱乐部或社群。 按用途分类 个人域名：主要用于个人博客、个人简历等。例如，yourname.com。 企业域名：用于企业官网、电子商务网站等，通常使用.com或.biz域名。 政府域名：专用于政府机构，通常使用.gov或各国的ccTLD，例如gov.uk。 教育域名：用于教育机构，通常使用.edu，例如harvard.edu。 组织域名：适用于非营利组织、非政府组织等，通常使用.org，例如redcross.org。 按地域分类 国际域名：这些域名可以被全球使用，如.com、.org、.net。 地区性域名：这些域名与特定国家或地区相关，使用对应的ccTLD，如： .cn：中国 .uk：英国 .ca：加拿大 .au：澳大利亚 按语种分类 国际化域名（Internationalized Domain Name, IDN）允许使用非ASCII字符（如中文、阿拉伯文、俄文等）注册域名。这使得非英语国家的用户可以使用自己的语言作为域名的一部分。例如，中国.com。\n域名可分为英文域名、中文域名、日文域名等。中文域名允许使用汉字进行注册，为使用中文的用户提供了更便捷的上网方式。\n……\n域名系统（DNS） DNS的功能：域名系统（DNS, Domain Name System）是互联网的“电话簿”，它将域名解析为相应的IP地址，帮助用户找到他们要访问的服务器。 DNS服务器：DNS服务器负责处理域名解析请求。通常包括根DNS服务器、顶级域名服务器、权威DNS服务器等。 根DNS服务器：管理顶级域名的根节点，如.com、.org的指向。 顶级域名服务器：管理某个TLD下所有二级域名的解析。 权威DNS服务器：提供特定域名的实际IP地址解析。 域名注册与管理 域名注册机构（Registrar）：域名注册机构是授权的公司或组织，提供域名注册服务。常见的域名注册机构有GoDaddy、Namecheap、阿里云等。\n域名注册步骤：\n选择域名：确定要注册的域名（如example.com）。 查询可用性：检查域名是否已被注册，如果没有，则可以继续注册。 选择注册商：选择一个域名注册机构进行注册。 支付费用：大多数域名注册需要按年收费，费用视TLD类型和注册年限而定。 域名解析设置：注册成功后，需要设置DNS记录，将域名指向对应的服务器IP地址。 DNS记录类型 在域名管理中，常见的DNS记录类型包括：\nA记录：将域名指向一个IPv4地址。 AAAA记录：将域名指向一个IPv6地址。 CNAME记录：将一个域名指向另一个域名。相对于取别名。 MX记录：用于指定处理电子邮件的服务器（邮件交换记录）。 TXT记录：存储任意文本信息，常用于验证域名所有权和设置电子邮件安全机制（如SPF、DKIM等）。 NS记录：指定负责解析该域名的DNS服务器。 域名解析过程 当用户在浏览器中输入一个域名时，解析过程如下：\n本地缓存查询：浏览器和操作系统首先检查本地是否缓存了该域名的IP地址。 DNS递归查询：如果本地没有缓存，系统会向递归DNS服务器发送查询请求。 根DNS查询：递归DNS服务器向根DNS服务器查询该域名所属的TLD服务器地址。 TLD服务器查询：递归DNS服务器向TLD服务器查询该域名的权威DNS服务器地址。 权威DNS查询：最终，递归DNS服务器向权威DNS服务器查询到该域名的IP地址，并返回给用户。 域名的安全性 域名劫持：攻击者通过劫持DNS解析过程，将合法域名指向恶意网站。 域名仿冒：攻击者注册与目标域名非常相似的域名，欺骗用户（如gooogle.com）。 DNSSEC：DNSSEC（Domain Name System Security Extensions）是DNS的安全扩展，它通过数字签名验证DNS数据的真实性，防止域名劫持。 域名的过期与续费 域名过期：域名注册有时限，通常为1年到10年。若到期未续费，域名将被标记为“过期”，可能会被其他人注册。 一旦域名过期，一定要及时处理。要不然可能会出现官网跳yellow的情况。 赎回期：在域名过期后的短时间内（通常是30天到45天），域名进入赎回期，原注册者可以以较高费用赎回域名。 删除期：赎回期结束后，域名进入删除期，最终会被删除并重新开放注册。 域名的法律与争议 域名抢注：抢注指在他人注册商标或品牌之前，恶意注册与其相同或相似的域名，从而获利或损害对方利益。抢注行为通常会引发法律纠纷。 域名争议解决政策（UDRP）：这是ICANN制定的一套解决域名争议的政策，用于处理恶意注册和使用域名的纠纷。 域名的应用与趋势 品牌保护：企业通常会注册多个相关域名（如品牌名、商标名等）以保护品牌。 数字资产：优质域名具有很高的商业价值，短、易记或与热门关键词相关的域名常被视为数字资产，可能以高价交易。 新顶级域名的兴起：随着互联网的发展，越来越多的nTLD被引入，如.tech、.shop等，给企业和个人提供了更多选择。 常见的域名工具和服务 WHOIS查询：用于查询域名的注册信息，包括注册人、注册商、注册日期等。 DNS查询工具：用于检查域名的DNS记录，了解域名解析是否正确。 域名生成器：帮助用户根据关键字生成可用的域名建议。 SSL证书：在域名上配置SSL证书可以启用HTTPS，确保网站通信的安全性。 端口基础 端口（Port）在计算机网络中是用于标识特定进程或网络服务的逻辑终端。它是IP地址的一部分，用于在传输层（通常是传输控制协议 TCP 或用户数据报协议 UDP）上区分不同的网络应用程序。以下是关于端口的详细介绍：\n端口的基本概念 端口号：每个端口都有一个独特的数字标识，称为端口号。端口号的范围是0到65535，共计65536个可能的端口。\nIP地址与端口：一个IP地址可以通过不同的端口号与多个应用程序通信。IP地址定位到具体的设备，端口号则用于确定设备上的具体应用程序。\n端口号的分类 端口号根据用途和范围可以分为以下三类：\n知名端口（Well-Known Ports）、公认端口、保留端口：范围是0-1023。这些端口号通常被分配给系统或特定的网络服务。由于这些端口的重要性，通常不建议用户自行修改或占用这些端口。例如： HTTP：80（用于网页浏览） HTTPS：443（用于安全网页浏览） FTP：21（用于文件传输） SSH：22（用于安全远程登录） ssh服务器的默认端口号是22，如果是默认端口号，在连接的时候，可以省略。 Telnet：23（用于不安全的远程登录） SMTP：25（用于发送邮件） DNS：53（用于域名解析） POP3：110（用于接收邮件） IMAP：143（用于管理邮件） 注册端口（Registered Ports）：范围是1024-49151。这些端口号通常分配给用户进程或特定的应用程序服务。这些端口较为松散地绑定于一些服务，但并非特定于某一种服务。许多服务可能会绑定在这些端口上，并且这些端口也用于许多其他目的。用户或应用程序可以根据需要自定义这些端口，但应确保不会与已有的服务或应用程序产生冲突。例如： MySQL数据库：3306 PostgreSQL数据库：5432 动态/私有端口（Dynamic/Private Ports）：范围是49152-65535。这些端口号通常分配给临时或私有的客户端进程，通常在客户端与服务器通信时动态分配。理论上不应为常用服务所分配。 端口的工作原理 在网络通信中，IP地址用于标识一台主机，而端口号用于标识主机上的具体服务或应用程序。当一个应用程序在主机上运行时，它通常会监听特定的端口以等待客户端连接。\n例如，当你在浏览器中输入一个URL时，浏览器将会发出一个HTTP请求。这个请求会被发送到目标服务器的IP地址和端口80（默认的HTTP端口），服务器上的应用程序（例如Apache或Nginx）监听这个端口并处理请求，然后将响应返回给浏览器。\n端口的安全性建议 仅开放必要的端口：减少开放的端口数量可以降低攻击面。 使用防火墙：防火墙是保护端口安全的第一道防线。 监控和日志记录：监控端口的使用情况并保持详细的日志记录，有助于发现潜在的安全问题。 使用加密协议：如尽量使用HTTPS代替HTTP，以保护数据传输的安全性。 PS：更多内容请网络搜索。\n用户和权限的基本概念 用户是Linux系统工作中重要的一环，用户管理包括用户与组的管理。\n在Linux系统中，不论是由本机或是远程登录系统，每个系统都必须拥有一个账号，并且不同账号对于不同的系统资源拥有不同的使用权限。\n在Linux中，可以指定每一个用户针对不同的文件或目录的不同权限。\n对文件/目录的权限包括：\n权限 英文 缩写 数字代号 读 read r 4 写 write w 2 执行 excute x 1 组 为了方便用户管理，提出了组的概念。 在实际应用中，可以预先针对组设置好权限，然后将不同的用户添加到对应的组中，从而不用依次为每一个用户设置权限。 ls -l扩展 ls -l ：可以查看当前目录下文件/目录的详细信息，从左到右依次是：\n示意图：\n用空格进行分隔：\n1 2 3 4 localhost demo $ ls -lh total 20K -rw-r--r--. 2 root root 19K May 8 22:49 hard lrwxrwxrwx. 1 root root 18 May 8 23:46 soft -\u0026gt; /root/test/abc.txt 权限：\n第一个字母d表示目录，-表示文件，l表示软连接。 接着三位数表示：当前用户权限，就是拥有者，r：可读，w：可写，x：可执行 接着三位数表示：组权限，在Linux中，很多时候，会出现组名和用户名相同的情况，后续会介绍。 接着三位数表示：其他用户权限。 硬链接数\n通俗的来讲，就是有多少种方式，可以访问到这个文件/目录\n访问方式：\n绝对路径\ncd ..\n目录看有多少个子目录通过cd .. 访问。\n能通过这两种访问方式访问的硬链接数＋1。\n所以通常不包含子目录的硬链接数是2。注意是不包含子目录，但可以包含文件。\n参考：Linux Common Commands#ln\n一般文件都是1，因为只能通过路径访问。\n但是可以通过创建硬链接来改变硬链接数。 硬链接不能对目录创建。 拥有者：数字后面的字符\n所属组：所属组的名称，在Linux中，很多时候，会出现组名和用户名相同的情况，后续会介绍。\n文件大小\n可通过-h选项，以人类能读懂的方式显示。 最后修改时间\n文件/目录名称\n有的带箭头的是软连接。\n硬链接不会显示。但在有的shell中会高亮。 超级用户 Linux系统中root账号通常用于系统的维护和管理，对操作系统的所有资源具有所有访问权限。 在大多数版本的Linux中，都不推荐直接使用root账号登录系统。 在Linux安装的过程中，系统会自动创建一个用户账号，而这个默认的用户就称之为”标准用户“。 sudo： su是substitute user 的缩写，中文翻译为替代用户，表示使用另一个用户的身份。 sudo 命令用来以其他用户执行命令，预设的用户为root。就是以管理员身份运行命令。 用户使用sudo时，必须先输入密码，之后有5分钟的有效期限，超过期限则必须重新输入密码。 若未经过授权的用户企图使用sudo，则会发出警告邮件给管理员。 passwd文件 passwd文件是存放用户信息的文件，属于系统配置文件，因此存放于/etc/passwd。 存放形式：由6个分号组成的7个信息，分别是： 用户名 ：密码（x：表示加密的密码） ：UID（用户标识） ：GID（组标识） ：用户全名或本地账号（如果没有指定就用用户名） ：家目录 ：登录使用的Shell（就是登录之后，使用的终端命令窗口，大多是Linux发现版默认是/bin/bash） Linux常用基础命令* ls: 查看当前文件夹下的内容(list)：\n-a：显示当前文件夹下的所有内容，包括隐藏内容。 -l：显示文件、目录的详细信息。 -h：配合-l使用以人类读懂的方式显示文件大小。 可以配合通配符查看文件或目录： ls *.txt: 查看所txt文件。 pwd: 查看当前所在的文件夹(print wrok directory)。\ncd [目录名]：切换目录(change directory)：\ncd：切换到当前用户的主目录（/home/用户目录）。 cd ~：切换到当前用户的主目录（/home/用户目录）。 cd .: 切换到当前目录，保存当前目录不变。 cd ..：切换到上一级目录。 cd -：可以来回切换最近两次的工作目录。 touch [文件名]：新键文件(touch)：\n如果文件不存在，新键文件。 如果目标存在，可以修改文件或目录的最后访问时间和修改时间。 mkdir [目录名]：创建目录(make derectoy)：\n-p：可以递归创建多个目录如：mkdir -p a/b/c。 rm [文件名]：永久删除指定的文件名(remove)：\n-f: 强制删除文件，忽略文件不存在时的提示。 -r：加入这个选项可以递归删除文件夹。 clear: 清屏(clear)：\n与快捷键ctrl-l等效。 tree [目录名]：查看文件，目录的树形图，不指定目录为当前目录：\n通常Linux的发行版，不会自带tree命令，需要使用对应的包管理器安装tree。 -d：只显示目录。 -f：显示完整路径。 -a：显示隐藏文件。 -L 2：限制目录深度。 -h：以人类读懂的方式显示文件大小。 -P \u0026quot;*.txt\u0026quot;：仅显示匹配模式的文件或目录。 -I \u0026quot;*.log\u0026quot;：排除匹配模式的文件或目录。 -t：按文件修改时间排序。 --help：查看帮助信息。 cp 源文件 目标文件：复制文件(copy)：\n-i：覆盖文件前提示。 -r: 递归复制文件夹。 mv 源文件 目标文件：移动文件或者目录(move)还可以重命名：\n-i: 覆盖文件前提示。 cat 文件名：查看文件内容，创建文件，文件合并，追加文件内容等功能(concatenate连接)：\n当查看文件内容时，会一次性显示所有文件内容，适合查看文件内容较少的： -b：对输出的非空行编号。 -n：对输出的所有行编号。 Linux中还有一个nl命令和cat -b的效果等价。 more 文件名：分屏显示文件内容，每次只显示一页内容，适合查看文件内容较多的(more)：\n操作键： 空格键：显示下一屏 enter：滚动下一行 b：回滚一屏 f：前滚一屏 q：退出 /word: 搜索word字符串 h：查看命令帮助信息。 其他：\necho 文字内容：在终端中回显文字内容，通常和重定向联合使用。 重定向：Linux允许将命令执行结果重定向到一个文件，就是将显示到终端的内容输出或者追加到指定文件中： \u0026gt;: 一个大于号表示输出，会覆盖文件原有的内容。 \u0026gt;\u0026gt;：两个大于号表示追加，会将内容追加到已有文件的末尾。 |：管道。第一个命令|第二个命令：第一个命令的输出通过管道作为第二个命令的输入。 常用管道命令的有： more：分屏显示内容。 grep：在命令执行结果的基础上查询指定的文本。 grep keyword：只显示包含关键字的行。过滤作用：\n-n：显示匹配行及行号。 -v:显示不包含匹配文本的所有行（相当于取反）。 -i：忽略大小写匹配。 模式查找：就是正则表达式： ^a: 搜索以a开头的行。 b$: 搜索以b结尾的行。 如: 搜索本机IP地址：ip address|grep inet。 shutdown [选项] [时间]：安全的关机/重启：不指定选项和参数表示默认一分钟之后关机，远程维护服务器最好不要关闭系统，而应该重新启动系统：\n-r：默认一分钟之后重启。reboot立即重启， -c: 取消之前指定的关机计划。 -t 时间单位秒：表示多少秒只后执行计划。 时间： now：现在。 +数字：表示10分钟之后执行计划。 如+10：表示十分钟之后执行计划。 具体时间：表示几点执行计划。 Linux基础进阶* 查看或配置网卡信息 【了解】很复杂😂\nifconfing: 查看计算机当前的网卡配置信息。目前现代大多数Linux发行版已经弃用或未安装该命令。用ip命令取代。 ifconfig：查看网卡配置信息。可用用ip addr替代。addr是address的缩写，还可以直接缩写为ip a。 ifconfig|grep inet: 查看网卡对于的IP地址。可用ip addr|grep inet替代。 ping ip地址或域名：检测目标主机是否连接正常，数值越大，速度越慢。 ping 127.0.0.1：检测本地网卡工作是否正常。 远程登录和复制文件 ssh客户端的简单使用：\n远程登录：ssh [-p port] username@hostname： username: 是远程机器上的用户名，如果不指定的话默认为当前本地主机登录的用户。 hostname：是远程机器的地址，可以是IP/域名，或者是后面会提到的别名。 port：是ssh server监听的端口号，如果不指定，就为默认值22 。\n使用exit可以退出当前用户的登录。或者快捷键ctrl-d。\nssh这个终端命令只能在Linux或者Unix（macOS）系统下使用。因为通常自带ssh客户端。\n如果在Windows系统中，可以安装putty或者xshell客户端软件使用即可，二者对个人均免费，建议搜索官网安装 putty： https://www.chiark.greenend.org.uk/~sgtatham/putty/\nxshell： www.xshell.com\n补充：现代的Windows版本一般也自动ssh客户端，如何没有，可以搜索添加可选功能进行安装。\nssh服务器的端口号可能不是22，如果遇到这种情况就需要使用-p选项，指定正确的端口号，否则无法正常连接到服务器。\n远程复制文件scp，不需要登录到远程计算机：\nscp：就是secure copy，是ssh客户端提供的一个工具，用来进行远程拷贝文件的命令。\n它的地址格式与ssh基本相同，需要注意的是，在指定端口是用的是大写的-P，而不是小写的p。\n-p是包留原文件或目录的元信息，如修改时间，访问时间等。\n-P: 若远程ssh服务器的端口号不是22，需要使用大写字母-P选项来指定端口号。\n-r: 递归复制目录\n将客户端文件复制到服务器：scp [-P port] 源文件 user@remote:目标地址 。\n将服务器文件复制到客户端：scp p[-P port] user@remote:源文件 目标地址。\n只要安装了ssh客户端（由openssh提供），就能使用scp命令。\nsftp：在文件传输时，使用的FTP服务而不是ssh服务，因此端口号为21。【了解】\nchmod基本使用 【重要】\nchmod可以修改用户、组、其他用户对文件、目录的权限。权限决定了谁可以读取、写入或执行文件或目录。chmod 允许你通过各种方式设置文件权限，包括符号模式和数字模式。下面是对 chmod 使用的详细介绍:\n文件权限基础 文件或目录的权限由三部分组成：\n用户（User, u）：文件的所有者。 组（Group, g）：与文件所有者属于同一组的用户。 其用户（Others, o）：除了所有者和组外的其他用户。 每个部分都包含三种权限：\n读取（Read, r）：可以读取文件的内容或列出目录的内容。 写入（Write, w）：可以修改文件的内容或在目录中创建、删除文件。 执行（Execute, x）：可以执行文件（如果它是一个脚本或程序），或进入目录。 默认情况下，新键的文件或目录：\n拥有者：文件或目录wrx。 主组（所属组）：目录rx，文件r。 其他用户：目录rx，文件r。 符号模式 chmod 的符号模式允许你使用符号表示权限的更改。符号模式由三个部分组成：\n用户类别：u（用户），g（组），o（其他），a（所有）。 操作符：+（添加权限），-（移除权限），=（设置精确权限，用,分隔，没有权限就是等号）。 权限：r（读取），w（写入），x（执行）。 注意： +w、-w只能给当前用户添加、移除可写权限。是为了安全，如果别的有w权限，有警告提示。 如果要所有：a+w、a-w。 +r、-r、+x、-x能够给所有用户添加、移除对应权限。 示例：\n给文件添加执行权限：chmod +x filename。这将对所有用户类别（用户、组、其他）添加执行权限。 给文件的所有者添加写权限：chmod u+w filename。 移除其他用户的读取权限：chmod o-r filename。 设置文件的权限为所有者可以读写，组可以读，其他人没有权限：chmod u=wr,g=r,o= filename。 数字模式 chmod 的数字模式通过使用八进制数表示权限。每种权限的八进制表示如下：\nr（读取） = 4 w（写入） = 2 x（执行） = 1 没有任何权限用0表示 补充：目录需要x权限才能打开，有r权限才能读取文件内容。 每个部分（用户、组、其他）的权限值相加后，形成一个三位数的八进制数，例如：\n7（4 + 2 + 1）= 读、写、执行权限 6（4 + 2 + 0）= 读、写权限 5（4 + 0 + 1）= 读、执行权限 4（4 + 0 + 0）= 只读权限 没有任何权限用0表示 示例：\n设置文件的权限为所有者可以读写执行，组和其他用户只能读取：chmod 744 filename。 设置文件的权限为所有用户类别可以读写执行：chmod 777 filename。 设置文件的权限为所有者可以读写，组和其他用户没有任何权限：chmod 600 filename。 递归更改目录权限 使用 -R 选项可以递归地更改目录及其子目录中所有文件、目录的权限。如果没有只能修改指定目录权限，里面的文件或目录权限无法修改。递归修改文件、目录权限。 将目录及其所有子文件和子目录的权限设置为 755：chmod -R 755 directoryName。 特殊权限位 【了解】\n在 Linux 和 Unix 中，还有一些特殊权限位，可以用 chmod 设置：\nSUID（Set User ID, u+s）：执行文件时，以文件所有者的权限运行，而不是执行者的权限。 SGID（Set Group ID, g+s）：目录下新创建的文件继承目录的组权限，而不是创建者的默认组。 粘滞位（Sticky Bit, o+t）：只允许文件的所有者或root用户删除文件，常用于公共目录（如 /tmp）。 小结 chmod 是一个功能强大的工具，可以灵活地控制文件和目录的权限。通过理解符号模式和数字模式，以及如何使用特殊权限位，你可以精确地控制谁可以访问和操作系统中的文件和目录。\n组管理基础 【了解】\n在 Linux 系统中，用户和组的管理是系统安全和权限控制的重要组成部分。组（Group）是用户的集合，用于简化权限管理。同一个组的成员可以共享对文件和目录的访问权限。下面详细介绍与 Linux 组管理相关的知识：\n组的概念 组（Group）：组是多个用户的集合，便于对文件和资源进行权限管理。组可以有一个或多个用户，一个用户也可以属于多个组。\n主组（Primary Group）：用户的主组是默认关联的组，每个用户在登录时默认隶属于主组。如果没有特殊指定默认主组与用户名同名。\n附加组（Supplementary Group）：用户除主组外，还可以属于其他多个附加组。\n组的相关文件 /etc/group：这个文件包含了系统中所有组的信息，包括组名、组ID（GID）、组成员等。每一行表示一个组，格式如下：组名:密码占位符:组ID:组成员列表。 例如：developers:x:1001:alice,bob。 /etc/passwd：虽然这个文件主要存储用户信息，但它也包含了用户的主组信息。在这个文件的每一行中，第 4 个字段代表用户的主组 ID（GID）。前面有介绍。 组的基本管理命令 ​\t以下是管理 Linux 组的常用命令（注意：创建组、删除组的终端命令都需要通过管理员权限sudo执行）：\n创建组：\n使用 groupadd 命令可以创建一个新组：sudo groupadd groupname。GID随机。\n可以通过 -g 选项指定组的 GID（组ID已存在会报错）：sudo groupadd -g 1050 groupname。\n删除组：\n使用 groupdel 命令可以删除一个组：sudo groupdel groupname。\n修改组：\n使用 groupmod 命令可以修改组的属性，比如组名或 GID。\n​\t修改组名：sudo groupmod -n newgroupname oldgroupname。\n​\t修改 GID：sudo groupmod -g 1051 groupname。\n查看组信息：\n使用 getent group 命令可以查看特定组的信息：getent group groupname。多个组用空格分隔。\n​\t这个命令也可以查看特定用户信息：getent psswd username。多个组用空格分隔。\n​\t没有指定组或者用户名默认查询全部。\n这个命令从 /etc/group 文件或组数据库中提取信息。\n查看所有组信息：cat /etc/group。\n将用户添加到组：\n使用 usermod 命令可以将用户添加到一个或多个组：\n1 sudo usermod -aG groupname username 其中 -aG 表示将用户追加到附加组，而不从其他组中移除。如果不加 -a 选项，用户将被移除所有其他组，且只属于指定的组。\n注意：设置了用户的附件组之后，需要重新登录才能生效\n查看用户的组：\n使用 groups 命令可以查看某个用户所属的所有组：groups username。\n如果不指定用户名，则显示当前用户的所属的所有组。\n使用 id 命令也可以查看用户的 UID、GID 和所属的组：id username。\n文件和目录的组权限管理 ​\t在 Linux 中，文件和目录的权限由三部分组成：所有者权限、组权限和其他用户权限。文件或目录的组权限决定了所属组的用户可以执行哪些操作。\n​\n​\t使用 chgrp 命令可以更改文件或目录的所属组：sudo chgrp groupname filename/dictionaryName。\n​\t使用 -R 选项可以递归地更改目录及其内容的所属组：sudo chgrp -R groupname directoryname。\n设置默认组权限 ​\t在某些情况下，文件或目录可能需要继承特定的组权限。使用 SGID（Set Group ID） 位可以确保目录中新创建的文件或子目录自动继承父目录的组属性：sudo chmod g+s directoryname。\n特殊组 ​\tLinux 系统中有一些特殊的组，具有系统特定的功能：\nroot：超级用户组，拥有系统的完全访问权限。 wheel：在某些 Linux 发行版中，wheel 组的成员可以使用 sudo 提权。 sudo：sudo 组的成员可以使用 sudo 命令获得临时的超级用户权限。 组管理的实际应用场景 多用户协作：\n在开发环境中，不同的用户可能需要协作处理同一项目。例如，项目文件可以分配给一个开发组，组中的所有成员都可以访问和修改这些文件。通过将用户添加到项目组，并使用组权限管理文件访问，可以轻松实现多用户协作。\n限制访问：\n使用组可以限制对特定资源的访问。例如，系统中可能存在一些敏感信息文件，只有特定的组成员可以访问。通过将文件的组权限设置为只读或只执行，可以控制哪些用户可以查看或修改这些文件。\n小结 ​\t在实际应用中，可以预先针对组设置好权限，然后将不同的用户添加到对应的组中，从而不用依次为每一个用户设置权限。Linux 组管理是系统管理中的一项重要技能。通过创建和管理组，以及配置文件和目录的组权限，可以有效地控制多用户环境中的资源访问。这种机制不仅简化了权限管理，还提高了系统的安全性和可维护性。\n用户管理基础* Linux 用户管理是系统管理的重要组成部分，涉及创建、修改、删除用户及管理用户权限和环境等操作。掌握用户管理知识对于维护系统安全和确保正确的资源访问非常关键。以下是有关 Linux 用户管理的详细介绍。\n用户的概念 用户（User）：在 Linux 中，用户是能够登录系统并执行操作的实体。每个用户都有一个唯一的用户 ID（UID）和用户名。\n超级用户（Superuser）：root 用户是系统的超级用户，**拥有最高权限，能够执行所有系统操作。**对所有文件、目录拥有完全的访问权限。\n普通用户（Regular User）：普通用户的权限受限，通常只能访问和修改自己拥有的文件和资源。\n用户相关的文件 /etc/passwd：该文件存储所有用户的基本信息。每个用户的信息在一行中表示，字段以冒号分隔。格式如下：\n1 用户名:密码占位符:用户ID:组ID:描述:主目录:Shell 例如：\n1 alice:x:1001:1001:Alice:/home/alice:/bin/bash /etc/shadow：该文件存储用户的加密密码及密码相关的安全信息。为了安全起见，只有超级用户可以访问该文件。格式如下：\n1 用户名:加密密码:上次修改日期:最小天数:最大天数:警告天数:不活动天数:过期日期:保留字段 如何加密密码为!!表示密码暂未设置。\n/etc/group：该文件存储系统中的组信息，每个组的信息包括组名、组ID（GID）和组成员。【前面有介绍】\n以上文件都可以使用getent命令配合文件名获取特定用户的相关信息，如果没有指定用户，将获取全部。\n用户的基本管理命令 注意：创建用户、删除用户、修改其他用户密码的终端命令都需要通过管理员权限sudo执行。\n创建新用户 使用 useradd 命令可以创建新用户：sudo useradd username。，新键用户之后，用户信息会保存在/etc/passwd文件中\n常用的选项包括：\n-m：自动创建用户的主目录。\n-d：指定用户的主目录位置。 创建用户时，如果忘记添加-m选项自动创建新用户的家目录，最简单的方法就是删除用户，重新创建。 -s：指定用户的默认 shell。\n-g：指定用户的主组。默认与用户名同名。\n创建用户时，如果不指定所属组系统会默认创建一个和用户名同名的组名（主组）。 -G：指定用户的附加组。\n例如，创建一个名为 bob 的用户，自动创建主目录并指定 shell 为 /bin/bash：\n1 sudo useradd -m -s /bin/bash bob 一般默认shell都为/bin/bash。\n注意：\n默认使用useradd添加的用户是没有权限使用sudo以root身份执行命令的，可以使用以下命令，将用户添加到sudo附加组中：usermod -aG sudo 用户名。 如果没有sudo组，可以使用wheel组，一般在Centos、RedHat之类的主机使用wheel组。 也没有wheel组，可以手动创建sudo组。 创建sudo组，并将用户添加到该组中，并保证 /etc/sudoers 中有%sudo ALL=(ALL:ALL) ALL内容即可。 编辑sudoers文件可以使用visudo命令。 设置用户密码 使用 passwd 命令可以为用户设置或更改密码：sudo passwd username。输入新密码并确认即可。 直接使用，修改当前登录用户密码。 如果是普通用户，直接用passwd可以修改自己的账户密码。 不需要输入旧密码😂。 只有设置用户密码之后我们才能够登录或者远程连接到该用户。 注意区分： /etc/passwd是用于保存用户信息的文件，没有可执行的权限。 /usr/bin/passwd是用于修改用户密码的程序，有可执行的权限，因此可以用来修改密码。 修改用户 使用 usermod 命令可以修改现有用户的属性：sudo usermod [选项] username。\n常用选项包括：\n-l：修改用户名。 -d：更改主目录。 -m：在更改主目录时移动用户的文件到新主目录。 -s：更改用户的默认 shell。 -g：更改用户的主组。 -G：更改用户的附加组。 -aG：将用户添加到附加组而不移除其他组。 例如，将 bob 用户添加到 developers 组中：\n1 sudo usermod -aG developers bob 删除用户 使用 userdel 命令可以删除用户：sudo userdel username。不会删除该用户的家目录。\n常用选项包括：\n-r：删除用户的主目录及其所有文件。 例如，删除 bob 用户并删除其主目录：\n1 sudo userdel -r bob 查看用户信息 id 命令：显示用户的 UID、GID 以及所属的组：id username。 省略用户名为当前登录用户。 finger 命令：显示用户的详细信息（需安装 finger 软件包）：finger username。 以下三个命令，如果没有指定用户名，与直接访问对应文件一样： getent passwd username 命令：从 /etc/passwd 文件或用户数据库中提取用户信息。 getent group username 命令：从 /etc/group 文件或用户数据库中提取用户组信息。 getent shadow username 命令：从 /etc/shadow 文件或用户数据库中提取用户密码信息。 who ：查看当前所有登录的用户列表，最后面表示的是IP地址，0或空表示远程主机。 whoami：查看当前登录用户的用户名。 更改文件所有者 使用 chown 命令可以更改文件或目录的所有者：sudo chown ownername filename。 也可以同时更改文件的所属组：sudo chown ownername:groupname filename。 使用 -R 选项可以递归更改目录及其内容的所有者：sudo chown -R ownername:groupname directoryname。 更改文件权限 【参考chmod基本使用】\n用户的环境配置* 用户主目录 /home/username：每个用户都有一个主目录，用于存放用户的个人文件和配置文件。用户主目录通常位于 /home 目录下，超级用户 root 的主目录是 /root。\n用户配置文件 用户的环境配置文件通常位于其主目录中，包括：\n.bashrc：是一个非登录Bash shell 的配置文件，在每次打开新的非登录 shell 时被执行。非登录 shell 通常是在已经登录的用户下打开的新终端或使用命令如 bash 启动的新 shell。\n这个文件常用于设置别名、环境变量、函数、shell 提示符（PS1）、**路径（PATH）**等。示例：\n1 2 3 4 5 6 7 8 9 10 11 12 # 设置命令别名 alias ll=\u0026#39;ls -alF\u0026#39; alias la=\u0026#39;ls -A\u0026#39; # 设置环境变量 export PATH=$PATH:/usr/local/mybin # 自定义提示符 PS1=\u0026#39;[\\u@\\h \\W]\\$ \u0026#39; # 设置用户语言环境 export LANG=en_US.UTF-8 .bash_profile：是一个登录Bash shell 的配置文件，专门用于登录 shell。登录 shell 是指用户通过登录（如 SSH 登录或在控制台输入用户名和密码）进入系统时的 shell。\n.bash_profile 通常会包含对其他配置文件的引用，例如 .bashrc。\n**意思就是登录bash shell的配置文件会引用非登录bash shell的配置文件。**示例：\n1 2 3 4 5 6 7 8 9 10 # 设置环境变量 export PATH=$PATH:/usr/local/myapp/bin # 让 .bash_profile 也加载 .bashrc 的配置 if [ -f ~/.bashrc ]; then . ~/.bashrc fi # 启动应用程序 myapp \u0026amp; .profile ：是一个与 .bash_profile 类似的配置文件，但它是通用登录shell配置文件。它不仅适用于 Bash，还适用于其他兼容 POSIX 的 shell（如 Dash）。\n当 .bash_profile 不存在时，Bash 登录 shell 会读取 .profile。\n当然这个登录shell的配置文件也会读取非登录bash shell 的配置文件.bashrc。\n以上三个shell 的配置文件都可以用于设置别名、环境变量、函数、shell 提示符（PS1）、**路径（PATH）**等。\n如果使用的是bash shell，一般只需要配置.bashrc即可。 如果是zsh shell，一般配置.zshrc。 因为登录shell都会加载非登录shell的配置文件。更通用。 .bash_logout：是一个在用户退出登录bash shell 时执行的配置文件。\n这个文件通常用于清理用户环境、记录日志或执行一些退出时需要的命令。\n在用户注销或退出登录bash shell时执行。适用于执行一些退出时需要的操作，如删除临时文件、打印退出信息等。\n示例：\n1 2 3 4 5 6 7 8 # 清除屏幕 clear # 删除临时文件 rm -rf /tmp/mytempfiles # 记录用户退出时间 echo \u0026#34;User logged out at $(date)\u0026#34; \u0026gt;\u0026gt; ~/.logout_log .bash_aliases 是一个通常用于存放用户定义的别名的配置文件。\n一般情况下，这个文件并不会自动创建，但用户可以在 .bashrc 中引用它，用来集中管理别名。也不常用。\n主要用于组织和管理大量别名，便于在 .bashrc 中引用。\n通过将别名从 .bashrc 中抽离到 .bash_aliases，可以使 .bashrc 更加整洁易读。\n示例：\n1 2 3 4 5 6 7 8 # 在 .bashrc 中引用 .bash_aliases if [ -f ~/.bash_aliases ]; then . ~/.bash_aliases fi # 在 .bash_aliases 中定义别名 alias ll=\u0026#39;ls -alF\u0026#39; alias gs=\u0026#39;git status\u0026#39; 以下两个配置文件属于全局配置文件，适用于系统上所有用户：\n/etc/bashrc：类似于用户的 .bashrc，用于全局非登录 shell配置，对所有用户生效。\n注意这个文件名与使用的shell有关系，上面是bash，如何是zsh，则是zshrc。 /etc/profile：用于设置全局环境变量和启动命令，在所有用户的登录 shell 中执行。全局登录shell配置文件。\n注意：以上两个文件默认不会相互加载。让登录shell加载非登录shell需要额外配置：\n1 2 3 if [ -f /etc/bashrc ]; then . /etc/bashrc fi 全局配置示例：\n1 2 3 4 5 6 7 8 9 10 11 12 # /etc/profile 示例 export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # 设置全局 umask umask 022 # /etc/bashrc 示例 # 禁用 Ctrl-S/Ctrl-Q 流控制 stty -ixon # 设置全局别名 alias ls=\u0026#39;ls --color=auto\u0026#39; 配置文件的加载顺序：\n登录 shell：当用户通过登录方式进入系统（如通过 SSH、控制台登录）时，Bash 会按照以下顺序加载配置文件： /etc/profile ~/.bash_profile （如果不存在则加载 ~/.profile） ~/.bashrc （通常通过 ~/.bash_profile 或~/.profile引用） ~/.bash_logout（退出时加载） 非登录 shell：当用户在已登录的环境中启动一个新的终端或 Bash shell 时Bash 会按照以下顺序加载配置文件： /etc/bashrc ~/.bashrc 以上文件区别总结：\n.bashrc 用于非登录 shell，每次打开新终端时执行，适合定义别名、函数和 shell 提示符等。\n.bash_profile 和 .profile 用于登录 shell，适合定义用户环境变量和启动命令。.bash_profile 是 Bash 专用的，而 .profile 则是通用的，适用于其他 POSIX shell。\n.bash_logout 在退出登录 shell 时执行，适合清理环境和记录日志。\n/etc/profile 和 /etc/bash.bashrc 是全局配置文件，适用于系统上所有用户的登录和非登录 shell。\n小结 通过合理配置这些文件，用户可以定制适合自己需求的工作环境，同时也可以确保在不同的登录方式和 shell 环境下，系统行为的一致性。\n用户配额管理: ? 【了解】用户配额管理用于限制用户可以使用的磁盘空间和文件数量。\n启用磁盘配额 首先需要安装配额管理工具并启用磁盘配额功能：\n编辑 /etc/fstab 文件，为相应的文件系统添加 usrquota 和/或 grpquota 选项。例如：\n1 /dev/sda1 /home ext4 defaults,usrquota,grpquota 0 2 重新挂载文件系统：\n1 sudo mount -o remount /home 初始化配额数据库：\n1 sudo quotacheck -cum /home 启动配额服务：\n1 sudo quotaon /home 配置用户配额 使用 edquota 命令可以为用户配置配额：\n1 sudo edquota username 编辑配额信息，通常包括软限制（Soft Limit）和硬限制（Hard Limit）两部分：\n软限制：用户达到软限制后，仍然可以继续使用磁盘，但会收到警告。 硬限制：用户达到硬限制后，将无法再使用更多的磁盘空间。 特殊用户和系统用户 在 Linux 系统中，除了普通用户和超级用户外，还有一些特殊用户和系统用户。这些用户通常用于运行系统服务和守护进程，并不允许正常登录。\n系统用户：系统用户通常拥有较低的 UID（如 0-999），它们用于管理系统进程和服务。 伪用户：一些伪用户如 nobody、daemon 用于运行没有专门账户的系统进程。 用户管理的实际应用场景 多用户环境中的协作\n在企业环境中，不同用户可能需要协作完成任务。通过创建用户组并赋予特定权限，可以确保团队成员可以访问和操作相关文件，而其他人无法访问。\n系统安全性\n通过严格管理用户账户，分配合适的权限，并定期检查和更新密码策略，可以提高系统的安全性，防止未经授权的访问。\n小结 Linux 用户管理是系统管理的核心内容之一。通过熟练掌握用户创建、修改、删除、权限管理、配额管理等操作，系统管理员可以有效控制多用户环境中的资源访问，保障系统安全性和稳定性。\nLinux常用基础进阶命令 which 命令：可以查看执行命令所在位置。包括别名。\n可能出现cd命令找不到：因为cd这个终端命令是内置在系统内核中的，没有独立的文件，因此使用which无法找到cd命令的位置。 whereis 命令：可以参考命令所在的所有位置。不包括别名。\n切换用户：\nsu - username：切换指定用户，并切换至家目录，说明：-可以切换到用户的家目录，否则保持位置不变\nexit：退出当前登录用户，如果没有退出shell，突出shell可以使用快捷键ctrl-d。\nsu 不指定用户名，可以切换到root用户，但是不推荐使用，因为不安全 。\nstat [选项] 文件名：查看文件或目录详细信息。\n包括文件的大小、权限、所有者、修改时间等。最重要的是能够查看文件或目录的访问时间（atime）、修改时间（mtime）、更改时间（ctime）。\n运行 stat 命令后，输出信息可能包括以下内容：\nFile: 文件名称。\nSize: 文件大小（以字节为单位）。\nBlocks: 文件占用的块数。\nIO Block: 每次文件 I/O 操作的块大小。\nFile type: 文件类型（如普通文件、目录、符号链接等）。\nDevice: 文件所在的设备号。\nInode: 文件的 inode 号。\nLinks: 文件的硬链接数。\nAccess: 文件的权限（包括八进制表示）。\nUid: 文件所有者的用户 ID 及用户名。\nGid: 文件所属组的组 ID 及组名。\nAccess: 文件的最后访问时间。\nModify: 文件的最后修改时间。\nChange: 文件的 inode 信息最后改变的时间。\nBirth: 文件的创建时间（注意：并非所有文件系统都支持这个信息）。\n常用选项：\n-L：当文件是一个符号链接时，显示链接所指向的目标文件的信息，而不是符号链接本身的信息。\n-f：显示文件系统的信息，而不是单个文件的信息。\n显示文件系统的信息：stat -f /。这会显示根文件系统的信息，包括块大小、空闲块数等。\n-c：自定义输出格式，通过格式化字符串显示特定的信息。\n使用 -c 或 --format 选项可以自定义输出格式。常用的格式化占位符包括：\n%n：文件名。 %s：文件大小。 %f：十六进制的文件模式。 %F：文件类型。 %h：硬链接数。 %u：所有者的用户 ID。 %U：所有者的用户名。 %g：组 ID。 %G：组名。 %x：最后访问时间。 %y：最后修改时间。 %z：最后改变时间。 stat -c '%n %s %y' example.txt：输出文件名、文件大小和最后修改时间，使用自定义格式。\n-t：以简洁格式输出信息，信息以单行显示。\ntouch 文件或目录：可以修改文件或目录的最后修改时间或最后访问时间。\n使用-t 时间选项可以指定时间：touch -t 202408201435.22 filename：这将时间戳设定为 2024-08-20 14:35:22。 时间表示：use [[CC]YY]MMDDhhmm[.ss] instead of current time。 系统相关简单命令:\n本节内容主要是为了方便通过远程终端维护服务器时，查看服务器上当前系统日期和时间、磁盘空间占用情况、程序执行情况。这些终端命令基本上都是查询命令，通过这些命令对系统资源的使用情况有个了解。开始：\n日期和时间：\nLinux 系统中的日期和时间管理是非常重要的，涉及到系统日志、计划任务、文件时间戳等多个方面。下面将详细介绍 Linux 中与日期和时间相关的主要知识点：\n在 Linux 中，时间分为系统时间（system time又叫Local time）和硬件时间（hardware time又叫RTC time：Real Time Clock、BIOS时间）。系统时间是操作系统维护的当前时间，而硬件时间由计算机的硬件时钟维护。\n注意：系统关机或重启之后，系统时间会被硬件时间覆盖。\n查看和设置系统时间：\ndate：显示当前时间和日期：\n使用 + 后跟格式字符串，可以以自定义格式显示日期和时间：date +\u0026quot;%Y-%m-%d %H:%M:%S\u0026quot;。这个格式可以改写成：**date +\u0026quot;%F %T\u0026quot;**格式都为为：2024-08-20 21:54:42\n%F：格式化为完整的日期，格式为 YYYY-MM-DD。\n%T：格式化为完整的时间，格式为 HH:MM:SS。\n%x：日期表示 (如 08/20/2024)\n%X：时间表示 (如 08:55:12)\n**date +“%x %X”**格式为：08/20/2024 09:49:47 PM\n常见的格式化参数：%Y：年、%m：月、%d：日、%H：时、%M：分、%S：秒。\n设置系统时间： 需要使用管理员权限来设置时间。例如，设置为 2024-08-20 14:35:22：\n1 sudo date -s \u0026#34;2024-08-20 14:35:22\u0026#34; 注意：不推荐使用该命令修改系统时间，后面会介绍。\n查看和设置硬件时间：\n显示硬件时间：hwclock\n输出类似于：Tue 20 Aug 2024 10:46:51 PM CST -0.821040 seconds\n将系统时间写入硬件时间：sudo hwclock --systohc\n短选项是-w。\n将硬件时间写入系统时间：sudo hwclock --hctosys\n短选项是-s。\n时区管理：\n时区决定了系统显示的时间与 UTC 时间的差值。\n实际上Linux时间还有一个时间，叫做UTC时间（Universal Time Coordinated协调世界时间）：\n系统时间=UTC时间+时区的偏移量\n查看当前时区：使用 date 命令查看时区：date +'%Z %z'。输出类似于：CST +0800。\n%Z：时区名称 (如 UTC, CST)\n%z：时区偏移 (如 +0800)\n命令timedatectl、date、hwclock的输出也会包括当前时区的信息。\n补充：timedatectl：\ntimedatectl 是一个在 Linux 系统上用于管理时间和日期的命令行工具。\n查看当前时间和日期：\n命令：timedatectl 输出信息包括系统当前的本地时间（系统时间）、UTC 时间、RTC时间、时区、系统时钟是否同步，以及是否启用了 NTP。 设置系统时间和日期：\n命令：timedatectl set-time \u0026lt;时间\u0026gt; 示例：timedatectl set-time \u0026quot;2024-08-20 12:34:56\u0026quot; 可以手动设置系统的时间和日期。 手动设置日期时间，推荐使用这种方式。使用date -s不会将UTC时间与RTC时间同步，会造成关机或重启之后，时间修改失效的问题：因为该命令只修改了UTC时间，重启之后，RTC时间会覆盖UTC时间，造成系统时间未成功修改。解决该问题：使用该命名之后，还需要将系统时间写入硬件时间：hwclock -w。所以推荐使用timedatectl set-time \u0026lt;时间\u0026gt;手动修改时间，它会同步UTC时间与RTC时间。 设置时区：设置时区通常需要管理员权限。\n命令：timedatectl set-timezone \u0026lt;时区名称\u0026gt;\n示例：timedatectl set-timezone Asia/Shanghai\n可以将系统时区设置为指定的时区名称。\n查看可用的时区：\n命令：timedatectl list-timezones 列出所有可用的时区，可以通过管道符 | 配合 grep 命令筛选特定时区。 手动设置时区： 可以通过创建或链接 /etc/localtime 文件到特定时区文件来设置时区：\n1 sudo ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 时间同步：时间同步确保系统UTC时间与全球标准UTC时间保持一致。同步硬件时间：hwclock -w.\nntp 服务\nntp（Network Time Protocol）用于同步网络上的时间。一般需要安装。\n安装 ntp\n1 2 sudo apt install ntp # Ubuntu/Debian sudo yum install ntp # CentOS/Fedora 启动ntp服务\n1 2 3 sudo systemctl start ntpd # 启动 sudo systemctl enable ntpd # 开机自启 service ntpd status # 查看ntp服务状态 启用或禁用 NTP 同步：\n启用 NTP 同步：timedatectl set-ntp true 禁用 NTP 同步：timedatectl set-ntp false NTP 同步用于自动从时间服务器获取时间。 完成以上操作之后，就会自动同步网络时间。\n启动 ntpd 服务后，时间同步会自动进行，但初始同步可能需要一些时间，并且ntpd会采取渐进方式调整时钟。为了确保系统时间尽快同步，可以手动强制同步一次。\n强制立即同步时间：sudo ntpd -gq\n-g 选项允许在第一次同步时忽略时间偏差的大小。 -q 选项使 ntpd 在同步完成后立即退出，而不继续作为守护进程运行。 检查同步状态：ntpq -p。会显示与哪些 NTP 服务器同步及其状态。 * 号表示正在使用的服务器。\n1. cal：calendar查看日历，-y选项可以查看一年的日历 磁盘信息\ndf -h ：disk free显示磁盘剩余空间 du -h [目录名]：disk usage显示目录下的文件大小，省略目录名为当前目录 -h 选项以人性化的方式显示文件大小 进程信息\n所谓进程，通俗地说就是当前正在执行的一个程序 top：动态显示运行中的进程并且排序，要退出键盘输入q ps aux：process status查看进程的详细状况 ps默认只会显示当前用户通过终端启动的应用程序，另外这个命令的选项没有减号- a：显示终端上的所有进程，包括其他用户的进程 u：显示进程的详细状态 x：显示没有控制终端的进程 注意：使用kill命令时，最好只终止当前用户开启的进程，而不要终止root身份开启的进程，否则可能导致系统崩溃 kill [-9] 进程代号PID：终止指定代号的进程，-9选项表示强行终止 其他命令\n查找文件 find命令功能非常强大，通常用来在特定的目录下搜索符合条件的文件 find [路径] -name \u0026ldquo;*.py\u0026rdquo;：查找指定路径下扩展名是.py的文件，包括子目录 如果省略路径，表示在当前目录下查找 之前学习的通配符，在使用find命令时同时可用 有关find的高级使用，后面慢慢了解，匹配选项有多种模式，-name以名字搜索，后面会介绍更多 搜索包含1的文件或目录：find -name \u0026ldquo;1\u0026rdquo; 搜索以.txt为扩展名的文件：find -name \u0026ldquo;*.txt\u0026rdquo; 搜索以1开头的文件或目录：find -name \u0026ldquo;1*\u0026rdquo; 软连接 ln -s 被链接的源文件 链接文件：建立文件的软链接，用通俗的方式讲类似于windows下的快捷方式 没有-s选项建立的是一个硬链接文件 两个文件占用相同大小的磁盘空间，工作中几乎不会建立文件的硬链接 源文件要使用绝对路径，不能使用相对路径，这样可以方便移动链接文件后，仍然能够正常使用 硬链接（知道） 在使用ln创建链接时，如果没有-s选项，会创建一个硬链接，而不是软链接 软链接的过程：软链接文件名》软链接文件数据》文件名》文件数据 硬链接的过程：文件名》文件数据，硬链接》文件数据 在Linux中，文件名和文件的数据是分开存储的 在Linux中，只有文件的硬链接数==0才会被删除 使用ls -l可以查看一个文件的硬链接的数量 在日常工作中，几乎不会建立文件的硬链接，知道即可 打包压缩 打包压缩是日常工作中备份文件的一种方式 在不同的操作系统中，常用的打包压缩方式是不同的 Windows常用rar Mac常用zip Linux常用tar.gz 打包/解包 tar是Linux中常用的备份工具，此命令可以把一系列文件打包到一个大文件中，也可以把一个打包的大文件恢复成一系列文件 tar的命令格式如下： 打包文件：tar -cvf 打包的文件名.tar 被打包的文件/路径多个用空格隔开 不负责压缩 解包文件：tar -xvf 打包的文件名.tar -c：生成档案文件，创建打包文件 -x：解开档案文件 -v：列出归档的详细过程，显示进度 -f：指定档案文件的名称，f后面一定是.tar文件，所以必须放选项最后 f选项必须放在最后，其他选项顺序可以随意 压缩/解压缩 tar与gzip命令结合可以实现文件的打包和压缩 tar只负责打包文件，但不压缩 用gzip压缩tar打包后的文件，其扩展名一般用xxx.tar.gz 在Linux中，最常见的压缩文件格式就是xxx.tar.gz 在tar命令中有一个选项-z可以调用gzip，从而可以方便的实现压缩和解压缩的功能 压缩文件：tar -zcvf 打包的文件名.tar.gz 被压缩的文件/路径\u0026hellip; 解压缩文件：tar -zxvf 打包的文件名.tar.gz 解压缩到指定路径：tar -zxvf 打包的文件名.tar.gz -C 目标路径 -C：解压缩到指定目录，注意：要解压缩的目录必须存在 另外一种Linux中常见的压缩格式bzip2（two） tar与bzip2命令结合可以实现文件的打包和压缩（用法和gzip一样） tar只负责打包文件，但不压缩 用bzip2压缩tar打包后的文件，其扩展名一般用xxx.tar.bz2 在tar命令中有一个选项-j可以调用bzip2，从而可以方便的实现压缩和解压缩的功能 压缩文件：tar -jcvf 打包的文件名.tar.bz2 被压缩的文件/路径\u0026hellip; 解压缩文件：tar -jxvf 打包的文件名.tar.bz2 -C:同样适用 软件安装\n通过apt安装/卸载/更新软件 apt是advanced packaging tool，是Linux下的一款安装包管理工具 可以在终端中方便的安装/卸载/更新软件 安装软件：sudo apt install 安装的软件名软件包 安装命令其实不需要记，如果软件没有安装，终端会提示你安装 卸载软件：sudo apt remove 软件名 更新已安装的包：sudo apt upgrade，这种升级，如果软件包有相依性的问题，此软件包就不会被升级 更新已安装的包：sudo apt dist-upgrade，如果软件包有相依性的问题，会移除旧版，直接安装新版本 所以通常dist-upgrade会被认为是有点风险的升级 检查更新：apt update：注意：我们每次在执行更新操作之前都应该检查更新，在执行upgrade，如果没有执行update就没有获取到更新包的信息，那么upgrade就无效了 apt可以看作apt-get和apt-cache命令的子集，apt和apt-get功能一样，都是安装软件包，没有区别，但apt更方便使用 如：sudo apt install sl：一个小火车提示 一个比较漂亮的查看当前进程排名的软件：sudo apt install htop 配置软件源\n如果希望在Linux中安装软件，更加快速，可以通过设置镜像源，选择一个访问网速更快的服务器，来提供软件的下载/安装服务 提示：更换服务器之后，需要一个相对比较长时间的更新过程，需要耐心等待，更新完成后，在安装软件都会从新设置的服务器下载软件 所谓镜像源，就是所有服务器的内容都是相同的（镜像），但是根据所在位置不同，国内服务器通常速度会更快一些，因为Linux的服务器默认是国外 步骤 备份原先镜像源：sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup 输入sudo vim /etc/apt/sources.list命令进入源地址文件 按“i”进入插入模式 将原来的下载源注释调，在前面加一个#号 复制镜像源（其中一个） #aliyun 阿里云： deb http://mirrors.aliyun.com/kali kali-rolling main non-free contrib deb-src http://mirrors.aliyun.com/kali kali-rolling main non-free contrib #ustc 中科大 deb http://mirrors.ustc.edu.cn/kali kali-rolling main non-free contrib deb-src http://mirrors.ustc.edu.cn/kali kali-rolling main non-free contrib #tsinghua 清华 deb http://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main contrib non-free deb-src http://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main contrib non-free #浙大源 deb http://mirrors.zju.edu.cn/kali kali-rolling main contrib non-free deb-src http://mirrors.zju.edu.cn/kali kali-rolling main contrib non-free Esc退出插入模式 输入“:wq”保存退出 复制：在kali终端下，使用鼠标选中内容，就可以完成复制 粘贴：移动光标到需要粘贴的位置，按下鼠标中间的滚轮，就可以粘贴 deb代表软件的位置 deb-src代表软件的源代码的位置 您可以使用以下命令来查询 Linux 发行版：\nlsb_release -a:该命令适用于所有 Linux 系统，会显示出完整的版本信息，包括 Linux 系统的名称，如 Debian、Ubuntu、CentOS 等，和对应的版本号，以及该版本的代号。 uname -a:该命令可以查看当前操作系统的内核信息。 cat /proc/version:该命令可以查看当前操作系统的版本信息。 cat /etc/*-release:该命令可以查看当前操作系统的发行版信息。 ","date":"2024-08-16T23:23:25+08:00","permalink":"https://arlettebrook.github.io/p/linux-basics-introduction/","title":"Linux Basics Introduction"},{"content":" SSH 介绍 SSH（Secure Shell 的缩写）是一种网络协议，用于加密两台计算机之间的通信，并且支持各种身份验证机制。\n实际中，它主要用于保证远程登录和远程通信的安全，任何网络服务都可以用这个协议来加密。\nSSH 是什么 历史上，网络主机之间的通信是不加密的，属于明文通信。这使得通信很不安全，一个典型的例子就是服务器登录。登录远程服务器的时候，需要将用户输入的密码传给服务器，如果这个过程是明文通信，就意味着传递过程中，线路经过的中间计算机都能看到密码，这是很可怕的。\nSSH 就是为了解决这个问题而诞生的，它能够加密计算机之间的通信，保证不被窃听或篡改。它还能对操作者进行认证（authentication）和授权（authorization）。明文的网络协议可以套用在它里面，从而实现加密。\n历史 1995年，芬兰赫尔辛基工业大学的研究员 Tatu Ylönen 设计了 SSH 协议的第一个版本（现称为 SSH 1），同时写出了第一个实现（称为 SSH1）。\n当时，他所在的大学网络一直发生密码嗅探攻击，他不得不为服务器设计一个更安全的登录方式。写完以后，他就把这个工具公开了，允许其他人免费使用。\nSSH 可以替换 rlogin、TELNET、FTP 和 rsh 这些不安全的协议，所以大受欢迎，用户快速增长，1995年底已经发展到五十个国家的20,000个用户。SSH 1 协议也变成 IETF 的标准文档。\n1995年12月，由于客服需求越来越大，Tatu Ylönen 就成立了一家公司 SCS，专门销售和开发 SSH。这个软件的后续版本，逐渐从免费软件变成了专有的商业软件。\nSSH 1 协议存在一些安全漏洞，所以1996年又提出了 SSH 2 协议（或者称为 SSH 2.0）。这个协议与1.0版不兼容，在1997年进行了标准化，1998年推出了软件实现 SSH2。但是，官方的 SSH2 软件是一个专有软件，不能免费使用，而且 SSH1 的有些功能也没有提供。\n1999年，OpenBSD 的开发人员决定写一个 SSH 2 协议的开源实现，这就是 OpenSSH 项目。该项目最初是基于 SSH 1.2.12 版本，那是当时 SSH1 最后一个开源版本。但是，OpenSSH 很快就完全摆脱了原始的官方代码，在许多开发者的参与下，按照自己的路线发展。OpenSSH 随 OpenBSD 2.6 版本一起提供，以后又移植到其他操作系统，成为最流行的 SSH 实现。目前，Linux 的所有发行版几乎都自带 OpenSSH。\n现在，SSH-2 有多种实现，既有免费的，也有收费的。本书的内容主要是针对 OpenSSH。\nSSH 架构 SSH 的软件架构是服务器-客户端模式（Server - Client）。在这个架构中，SSH 软件分成两个部分：向服务器发出请求的部分，称为客户端（client），OpenSSH 的实现为 ssh；接收客户端发出的请求的部分，称为服务器（server），OpenSSH 的实现为 sshd。\n本教程约定，大写的 SSH 表示协议，小写的 ssh 表示客户端软件。\n另外，OpenSSH 还提供一些辅助工具软件（比如 ssh-keygen 、ssh-agent）和专门的客户端工具（比如 scp 和 sftp），这个教程也会予以介绍。\nSSH 客户端 简介 OpenSSH 的客户端是二进制程序 ssh。它在 Linux/Unix 系统的位置是/usr/local/bin/ssh。\n在Linux上安装SSH客户端:\n在大多数Linux发行版中，OpenSSH客户端通常默认已安装。如果没有安装，可以通过包管理器来安装。\n1 2 3 4 5 6 7 8 # Ubuntu 和 Debian $ sudo apt install openssh-client # CentOS 和 RHEL $ sudo yum install openssh-clients # Fedora $ sudo dnf install openssh-clients 在macOS上安装SSH客户端:\nmacOS预装了OpenSSH客户端，因此不需要额外安装。可以直接在终端使用ssh命令。\n在Windows上安装SSH客户端:\n从Windows 10版本1709开始，系统已经内置了OpenSSH客户端。可以通过以下步骤启用：\n打开设置 \u0026gt; 应用 \u0026gt; 可选功能(没有：搜索添加可选可能）。 向下滚动并找到“OpenSSH Client”，如果未安装，点击添加功能按钮。 在列表中找到“OpenSSH Client”，然后点击安装。 之后，可以在命令提示符（cmd）或PowerShell中使用ssh命令。\n或者使用封装了ssh客户端的工具：\n如Termius、FinalShell、WindTerm等。 安装以后，可以使用-V参数输出版本号，查看一下是否安装成功。\n1 $ ssh -V 基本用法 ssh 最常见的用途就是登录服务器，这要求服务器安装并正在运行 SSH 服务器软件(sshd)。\nssh 登录服务器的命令如下:\n1 $ ssh hostname 上面命令中，hostname是主机名，它可以是域名，也可能是 IP 地址或局域网内部的主机名。不指定用户名的情况下，将使用客户端的当前用户名，作为远程服务器的登录用户名。如果要指定用户名，可以采用下面的语法:\n1 $ ssh username@hostname 上面的命令中，用户名和主机名写在一起了，之间使用@分隔。\n用户名也可以使用ssh的-l参数指定，这样的话，用户名和主机名就不用写在一起了:\n1 $ ssh -l username hostname ssh 默认连接服务器的22端口，-p参数可以指定其他端口:\n1 $ ssh -p 8821 foo.com 上面命令连接服务器foo.com的8821端口，用户名为当前客户端主机登录的用户名。\n连接流程 ssh 连接远程服务器后，首先有一个验证过程，验证远程服务器是否为陌生地址。\n如果是第一次连接某一台服务器，命令行会显示一段文字，表示不认识这台机器，提醒用户确认是否需要连接。\n1 2 3 The authenticity of host \u0026#39;foo.com (192.168.121.111)\u0026#39; can\u0026#39;t be established. ECDSA key fingerprint is SHA256:Vybt22mVXuNuB5unE++yowF7lgA/9/2bLSiO3qmYWBY. Are you sure you want to continue connecting (yes/no)? 上面这段文字告诉用户，foo.com这台服务器的指纹是陌生的，让用户选择是否要继续连接（输入 yes 或 no）。\n所谓“服务器指纹”，指的是 SSH 服务器公钥的哈希值。每台 SSH 服务器都有唯一一对密钥，用于跟客户端通信，其中公钥的哈希值就可以用来识别服务器。\n在上面这段文字后面，输入yes，就可以将当前服务器的指纹也储存在本机~/.ssh/known_hosts文件中，并显示下面的提示。以后再连接的时候，就不会再出现警告了。\n1 Warning: Permanently added \u0026#39;foo.com (192.168.121.111)\u0026#39; (RSA) to the list of known hosts 然后，客户端就会跟服务器建立连接。接着，ssh 就会要求用户输入所要登录账户的密码。用户输入并验证密码正确以后，就能登录远程服务器的 Shell 了。\n下面的命令可以查看某个公钥的指纹。\n1 2 $ ssh-keygen -l -f /etc/ssh/ssh_host_ecdsa_key.pub 256 da:24:43:0b:2e:c1:3f:a1:84:13:92:01:52:b4:84:ff (ECDSA) 上面的例子中，ssh-keygen -l -f命令会输出公钥/etc/ssh/ssh_host_ecdsa_key.pub的指纹。\nssh 会将本机连接过的所有服务器公钥的指纹，都储存在本机的~/.ssh/known_hosts文件中。每次连接服务器时，通过该文件判断是否为陌生主机（陌生公钥）。\n加密参数 SSH 连接的握手阶段，客户端必须跟服务端约定加密参数集（cipher suite）。\n加密参数集包含了若干不同的加密参数，它们之间使用下划线连接在一起，下面是一个例子：\n1 TLS_RSA_WITH_AES_128_CBC_SHA 它的含义如下：\nTLS：加密通信协议 RSA：密钥交换算法 AES：加密算法 128：加密算法的强度 CBC：加密算法的模式 SHA：数字签名的 Hash 函数 下面是一个例子，客户端向服务器发出的握手信息：\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 Handshake protocol: ClientHello Version: TLS 1.2 Random Client time: May 22, 2030 02:43:46 GMT Random bytes: b76b0e61829557eb4c611adfd2d36eb232dc1332fe29802e321ee871 Session ID: (empty) Cipher Suites Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256” Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA Suite: TLS_RSA_WITH_AES_128_CBC_SHA Suite: TLS_RSA_WITH_3DES_EDE_CBC_SHA Suite: TLS_RSA_WITH_RC4_128_SHA Compression methods Method: null Extensions Extension: server_name Hostname: www.feistyduck.com Extension: renegotiation_info Extension: elliptic_curves Named curve: secp256r1 Named curve: secp384r1 Extension: signature_algorithms Algorithm: sha1/rsa Algorithm: sha256/rsa Algorithm: sha1/ecdsa Algorithm: sha256/ecdsa” 上面的握手信息（ClientHello）之中，Cipher Suites字段就是客户端列出可选的加密参数集，服务器在其中选择一个自己支持的参数集。\n服务器选择完毕之后，向客户端发出回应：\n1 2 3 4 5 6 7 8 9 10 11 Handshake protocol: ServerHello Version: TLS 1.2 Random Server time: Mar 10, 2059 02:35:57 GMT” Random bytes: 8469b09b480c1978182ce1b59290487609f41132312ca22aacaf5012 Session ID: 4cae75c91cf5adf55f93c9fb5dd36d19903b1182029af3d527b7a42ef1c32c80 Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 Compression method: null Extensions Extension: server_name Extension: renegotiation_info” 上面的回应信息（ServerHello）中，Cipher Suite字段就是服务器最终选定的加密参数。\n服务器密钥变更 服务器指纹可以防止有人恶意冒充远程主机。如果服务器的密钥发生变更（比如重装了 SSH 服务器），客户端再次连接时，就会发生公钥指纹不吻合的情况。这时，客户端就会中断连接，并显示一段警告信息。\n1 2 3 4 5 6 7 8 9 10 11 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! Someone could be eavesdropping on you right now (man-in-the-middle attack)! It is also possible that the RSA host key has just been changed. The fingerprint for the RSA key sent by the remote host is 77:a5:69:81:9b:eb:40:76:7b:13:04:a9:6c:f4:9c:5d. Please contact your system administrator. Add correct host key in /home/me/.ssh/known_hosts to get rid of this message. Offending key in /home/me/.ssh/known_hosts:36 上面这段文字的意思是，该主机的公钥指纹跟~/.ssh/known_hosts文件储存的不一样，必须处理以后才能连接。这时，你需要确认是什么原因，使得公钥指纹发生变更，到底是恶意劫持，还是管理员变更了 SSH 服务器公钥。\n如果新的公钥确认可以信任，需要继续执行连接，你可以执行下面的命令，将原来的公钥指纹从~/.ssh/known_hosts文件删除。\n1 $ ssh-keygen -R hostname 上面命令中，hostname是发生公钥变更的主机名。\n除了使用上面的命令，你也可以手工修改known_hosts文件，将公钥指纹删除。\n删除了原来的公钥指纹以后，重新执行 ssh 命令连接远程服务器，将新的指纹加入known_hosts文件，就可以顺利连接了。\nssh 命令行配置项 -c\n-c参数指定加密算法。\n1 2 3 $ ssh -c blowfish,3des server.example.com # 或者 $ ssh -c blowfish -c 3des server.example.com 上面命令指定使用加密算法blowfish或3des。\n-C\n-C参数表示压缩数据传输。\n1 $ ssh -C server.example.com -D\n-D参数指定本机的 Socks 监听端口，该端口收到的请求，都将转发到远程的 SSH 主机，又称动态端口转发，详见《端口转发》一章。\n1 $ ssh -D 1080 server 上面命令将本机 1080 端口收到的请求，都转发到服务器server。\n-f\n-f参数表示 SSH 连接在后台运行。\n-F\n-F参数指定配置文件。\n1 $ ssh -F /usr/local/ssh/other_config 上面命令指定使用配置文件other_config。\n-i\n-i参数用于指定私钥，意为“identity_file”，默认值为~/.ssh/id_dsa（DSA 算法）和~/.ssh/id_rsa（RSA 算法）。注意，对应的公钥必须存放到服务器，详见《密钥登录》一章。\n1 $ ssh -i my-key server.example.com -l\n-l参数指定远程登录的账户名。\n1 2 3 $ ssh -l sally server.example.com # 等同于 $ ssh sally@server.example.com -L\n-L参数设置本地端口转发，详见《端口转发》一章。\n1 $ ssh -L 9999:targetServer:80 user@remoteserver 上面命令中，所有发向本地9999端口的请求，都会经过remoteserver发往 targetServer 的 80 端口，这就相当于直接连上了 targetServer 的 80 端口。\n-m\n-m参数指定校验数据完整性的算法（message authentication code，简称 MAC）。\n1 $ ssh -m hmac-sha1,hmac-md5 server.example.com 上面命令指定数据校验算法为hmac-sha1或hmac-md5。\n-N\n-N参数用于端口转发，表示建立的 SSH 只用于端口转发，不能执行远程命令，这样可以提供安全性，详见《端口转发》一章。\n-o\n-o参数用来指定一个配置命令。\n1 $ ssh -o \u0026#34;Keyword Value\u0026#34; 举例来说，配置文件里面有如下内容。\n1 2 User sally Port 220 通过-o参数，可以把上面两个配置命令从命令行传入。\n1 $ ssh -o \u0026#34;User sally\u0026#34; -o \u0026#34;Port 220\u0026#34; server.example.com 使用等号时，配置命令可以不用写在引号里面，但是等号前后不能有空格。\n1 $ ssh -o User=sally -o Port=220 server.example.com -p\n-p参数指定 SSH 客户端连接的服务器端口。\n1 $ ssh -p 2035 server.example.com 上面命令连接服务器的2035端口。\n-q\n-q参数表示安静模式（quiet），不向用户输出任何警告信息。\n1 2 $ ssh –q foo.com root’s password: 上面命令使用-q参数，只输出要求用户输入密码的提示。\n-R\n-R参数指定远程端口转发，详见《端口转发》一章。\n1 $ ssh -R 9999:targetServer:902 local 上面命令需在跳板服务器执行，指定本地计算机local监听自己的 9999 端口，所有发向这个端口的请求，都会转向 targetServer 的 902 端口。\n-t\n-t参数在 ssh 直接运行远端命令时，提供一个互动式 Shell。\n1 $ ssh -t server.example.com emacs -v\n-v参数显示详细信息。\n1 $ ssh -v server.example.com -v可以重复多次，表示信息的详细程度，比如-vv和-vvv。\n1 2 3 $ ssh -vvv server.example.com # 或者 $ ssh -v -v -v server.example.com 上面命令会输出最详细的连接信息。\n-V\n-V参数输出 ssh 客户端的版本。\n1 2 $ ssh –V ssh: SSH Secure Shell 3.2.3 (non-commercial version) on i686-pc-linux-gnu 上面命令输出本机 ssh 客户端版本是SSH Secure Shell 3.2.3。\n-X\n-X参数表示打开 X 窗口转发。\n1 $ ssh -X server.example.com -1，-2\n-1参数指定使用 SSH 1 协议。\n-2参数指定使用 SSH 2 协议。\n1 $ ssh -2 server.example.com -4，-6\n-4指定使用 IPv4 协议，这是默认值。\n1 $ ssh -4 server.example.com -6指定使用 IPv6 协议。\n1 $ ssh -6 server.example.com 执行远程命令 SSH 登录成功后，用户就进入了远程主机的命令行环境，所看到的提示符，就是远程主机的提示符。这时，你就可以输入想要在远程主机执行的命令。\n另一种执行远程命令的方法，是将命令直接写在ssh命令的后面。\n1 $ ssh username@hostname command 上面的命令会使得 SSH 在登录成功后，立刻在远程主机上执行命令command。命令执行完成之后会自动退出连接。\n多行命令用引号或双引号括起来。\n下面是一个例子。\n1 $ ssh foo@server.example.com cat /etc/hosts 上面的命令会在登录成功后，立即远程执行命令cat /etc/hosts。\n采用这种语法执行命令时，ssh 客户端不会提供互动式的 Shell 环境，而是直接将远程命令的执行结果输出在命令行。但是，有些命令需要互动式的 Shell 环境，这时就要使用-t参数。\n1 2 3 4 5 6 # 报错 $ ssh remote.server.com emacs emacs: standard input is not a tty # 不报错 $ ssh -t server.example.com emacs 上面代码中，emacs命令需要一个互动式 Shell，所以报错。只有加上-t参数，ssh 才会分配一个互动式 Shell。\n不是交互式命令，使用-t参数之后，也会立即退出连接。交互命令需要主动退出。\n客户端配置文件 位置 SSH 客户端的全局配置文件是/etc/ssh/ssh_config，用户个人的配置文件在~/.ssh/config，优先级高于全局配置文件。\n除了配置文件，~/.ssh目录还有一些用户个人的密钥文件和其他文件。下面是其中一些常见的文件：\n~/.ssh/id_ecdsa：用户的 ECDSA 私钥。 ~/.ssh/id_ecdsa.pub：用户的 ECDSA 公钥。 ~/.ssh/id_rsa：用于 SSH 协议版本2 的 RSA 私钥。 ~/.ssh/id_rsa.pub：用于SSH 协议版本2 的 RSA 公钥。 ~/.ssh/identity：用于 SSH 协议版本1 的 RSA 私钥。 ~/.ssh/identity.pub：用于 SSH 协议版本1 的 RSA 公钥。 ~/.ssh/known_hosts：包含 SSH 服务器的公钥指纹。 主机设置 用户个人的配置文件~/.ssh/config，可以按照不同服务器，列出各自的连接参数，从而不必每一次登录都输入重复的参数。\n下面是一个例子：\n1 2 3 4 5 6 7 Host * Port 2222 Host remoteserver HostName remote.example.com User neo Port 2112 上面代码中，Host *表示对所有主机生效，后面的Port 2222表示所有主机的默认连接端口都是2222，这样就不用在登录时特别指定端口了。这里的缩进并不是必需的，只是为了视觉上，易于识别针对不同主机的设置。\n后面的Host remoteserver表示，下面的设置只对主机remoteserver生效。remoteserver只是一个别名，具体的主机由HostName命令指定，User和Port这两项分别表示用户名和端口。这里的Port会覆盖上面Host *部分的Port设置。\n以后，登录remote.example.com时，只要执行ssh remoteserver命令，就会自动套用 config 文件里面指定的参数。 单个主机的配置格式如下：\n1 2 3 $ ssh remoteserver # 等同于 $ ssh -p 2112 neo@remote.example.com Host命令的值可以使用通配符，比如Host *表示对所有主机都有效的设置，Host *.edu表示只对一级域名为.edu的主机有效的设置。它们的设置都可以被单个主机的设置覆盖。\n配置命令的语法 ssh 客户端配置文件的每一行，就是一个配置命令。配置命令与对应的值之间，可以使用空格，也可以使用等号。\n1 2 3 Compression yes # 等同于 Compression = yes #开头的行表示注释，会被忽略。空行等同于注释。\n主要配置命令 下面是 ssh 客户端的一些主要配置命令，以及它们的范例值。\nAddressFamily inet：表示只使用 IPv4 协议。如果设为inet6，表示只使用 IPv6 协议。 BindAddress 192.168.10.235：指定本机的 IP 地址（如果本机有多个 IP 地址）。 CheckHostIP yes：检查 SSH 服务器的 IP 地址是否跟公钥数据库吻合。 Ciphers blowfish,3des：指定加密算法。 Compression yes：是否压缩传输信号。 ConnectionAttempts 10：客户端进行连接时，最大的尝试次数。 ConnectTimeout 60：客户端进行连接时，服务器在指定秒数内没有回复，则中断连接尝试。 DynamicForward 1080：指定动态转发端口。 GlobalKnownHostsFile /users/smith/.ssh/my_global_hosts_file：指定全局的公钥数据库文件的位置。 Host server.example.com：指定连接的域名或 IP 地址，也可以是别名，支持通配符。Host命令后面的所有配置，都是针对该主机的，直到下一个Host命令为止。 HostKeyAlgorithms ssh-dss,ssh-rsa：指定密钥算法，优先级从高到低排列。 HostName myserver.example.com：在Host命令使用别名的情况下，HostName指定域名或 IP 地址。 IdentityFile keyfile：指定私钥文件。 LocalForward 2001 localhost:143：指定本地端口转发。 LogLevel QUIET：指定日志详细程度。如果设为QUIET，将不输出大部分的警告和提示。 MACs hmac-sha1,hmac-md5：指定数据校验算法。 NumberOfPasswordPrompts 2：密码登录时，用户输错密码的最大尝试次数。 PasswordAuthentication no：指定是否支持密码登录。不过，这里只是客户端禁止，真正的禁止需要在 SSH 服务器设置。 Port 2035：指定客户端连接的 SSH 服务器端口。 PreferredAuthentications publickey,hostbased,password：指定各种登录方法的优先级。 Protocol 2：支持的 SSH 协议版本，多个版本之间使用逗号分隔。 PubKeyAuthentication yes：是否支持密钥登录。这里只是客户端设置，还需要在 SSH 服务器进行相应设置。 RemoteForward 2001 server:143：指定远程端口转发。 SendEnv COLOR：SSH 客户端向服务器发送的环境变量名，多个环境变量之间使用空格分隔。环境变量的值从客户端当前环境中拷贝。 ServerAliveCountMax 3：如果没有收到服务器的回应，客户端连续发送多少次keepalive信号，才断开连接。该项默认值为3。 ServerAliveInterval 300：客户端建立连接后，如果在给定秒数内，没有收到服务器发来的消息，客户端向服务器发送keepalive消息。如果不希望客户端发送，这一项设为0。 StrictHostKeyChecking yes：yes表示严格检查，服务器公钥为未知或发生变化，则拒绝连接。no表示如果服务器公钥未知，则加入客户端公钥数据库，如果公钥发生变化，不改变客户端公钥数据库，输出一条警告，依然允许连接继续进行。ask（默认值）表示询问用户是否继续进行。 TCPKeepAlive yes：客户端是否定期向服务器发送keepalive信息。 User userName：指定远程登录的账户名。 UserKnownHostsFile /users/smith/.ssh/my_local_hosts_file：指定当前用户的known_hosts文件（服务器公钥指纹列表）的位置。 VerifyHostKeyDNS yes：是否通过检查 SSH 服务器的 DNS 记录，确认公钥指纹是否与known_hosts文件保存的一致。 SSH 密钥登录 SSH 默认采用密码登录，这种方法有很多缺点，简单的密码不安全，复杂的密码不容易记忆，每次手动输入也很麻烦。密钥登录是比密码登录更好的解决方案。\n密钥是什么 密钥（key）是一个非常大的数字，通过加密算法得到。对称加密只需要一个密钥，非对称加密需要两个密钥成对使用，分为公钥（public key）和私钥（private key）。\nSSH 密钥登录采用的是非对称加密，每个用户通过自己的密钥登录。其中，私钥必须私密保存，不能泄漏；公钥则是公开的，可以对外发送。它们的关系是，公钥和私钥是一一对应的，每一个私钥都有且仅有一个对应的公钥，反之亦然。\n如果数据使用公钥加密，那么只有使用对应的私钥才能解密，其他密钥都不行；反过来，如果使用私钥加密（这个过程一般称为“签名”），也只有使用对应的公钥解密。\n密钥登录的过程 SSH 密钥登录分为以下的步骤:\n预备步骤，客户端通过ssh-keygen生成自己的公钥和私钥。\n第一步，手动将客户端的公钥放入远程服务器的指定位置。\n第二步，客户端向服务器发起 SSH 登录的请求。\n第三步，服务器收到用户 SSH 登录的请求，发送一些随机数据给用户，要求用户证明自己的身份。\n第四步，客户端收到服务器发来的数据，使用私钥对数据进行签名，然后再发还给服务器。\n第五步，服务器收到客户端发来的加密签名后，使用对应的公钥解密，然后跟原始数据比较。如果一致，就允许用户登录。\nssh-keygen命令：生成密钥 基本用法 密钥登录时，首先需要生成公钥和私钥。OpenSSH 提供了一个工具程序ssh-keygen命令，用来生成密钥。\n直接输入ssh-keygen，程序会询问一系列问题，然后生成密钥:\n1 $ ssh-keygen 通常做法是使用-t参数，指定密钥的加密算法:\n1 $ ssh-keygen -t dsa 上面示例中，-t参数用来指定密钥的加密算法，一般会选择 DSA 算法或 RSA 算法。如果省略该参数，默认使用 RSA 算法。\n一般都加-t选项，有的ssh版本默认的加密算法不同。\n输入上面的命令以后，ssh-keygen会要求用户回答一些问题:\n1 2 3 4 5 6 7 8 9 $ ssh-keygen -t dsa Generating public/private dsa key pair. Enter file in which to save the key (/home/username/.ssh/id_dsa): press ENTER Enter passphrase (empty for no passphrase): ******** Enter same passphrase again: ******** Your identification has been saved in /home/username/.ssh/id_dsa. Your public key has been saved in /home/username/.ssh/id_dsa.pub. The key fingerprint is: 14:ba:06:98:a8:98:ad:27:b5:ce:55:85:ec:64:37:19 username@shell.isp.com 上面示例中，执行ssh-keygen命令以后，会出现第一个问题，询问密钥保存的文件名，默认是~/.ssh/id_dsa文件，这个是私钥的文件名，对应的公钥文件~/.ssh/id_dsa.pub是自动生成的。用户的密钥一般都放在主目录的.ssh目录里面。\n如果选择rsa算法，生成的密钥文件默认就会是~/.ssh/id_rsa（私钥）和~/.ssh/id_rsa.pub（公钥）。\n接着，就会是第二个问题**，询问是否要为私钥文件设定密码保护（passphrase）。这样的话，即使入侵者拿到私钥，还是需要破解密码**。如果为了方便，不想设定密码保护，可以直接按回车键，密码就会为空。后面还会让你再输入一次密码，两次输入必须一致。注意，这里“密码”的英文单词是 passphrase，这是为了避免与 Linux 账户的密码单词 password 混淆，表示这不是用户系统账户的密码。\n秘钥密码如果不为空，那么每次连接都需要输入密码。\n最后，就会生成私钥和公钥，屏幕上还会给出公钥的指纹，以及当前的用户名和主机名作为注释，用来识别密钥的来源。\n公钥文件和私钥文件都是文本文件，可以用文本编辑器看一下它们的内容。公钥文件的内容类似下面这样:\n1 2 3 ssh-dss AAAAB3NzaC1yc2EAAAABIwAAAIEAvpB4lUbAaEbh9u6HLig7amsfywD4fqSZq2ikACIUBn3GyRPfeF93l/ weQh702ofXbDydZAKMcDvBJqRhUotQUwqV6HJxqoqPDlPGUUyo8RDIkLUIPRyq ypZxmK9aCXokFiHoGCXfQ9imUP/w/jfqb9ByDtG97tUJF6nFMP5WzhM= username@shell.isp.com 上面示例中，末尾的username@shell.isp.com是公钥的注释，用来识别不同的公钥，表示这是哪台主机（shell.isp.com）的哪个用户（username）的公钥，不是必需项。\n注意，公钥只有一行。因为它太长了，所以上面分成三行显示。\n下面的命令可以列出用户所有的公钥:\n1 $ ls -l ~/.ssh/id_*.pub 生成密钥以后，建议修改它们的权限，防止其他人读取:\n1 2 $ chmod 600 ~/.ssh/id_rsa $ chmod 600 ~/.ssh/id_rsa.pub 配置项 ssh-keygen的命令行配置项，主要有下面这些:\n（1）-b\n-b参数指定密钥的二进制位数。这个参数值越大，密钥就越不容易破解，但是加密解密的计算开销也会加大。\n一般来说，-b至少应该是1024，更安全一些可以设为2048或者更高。\n（2）-C\n-C参数可以为密钥文件指定新的注释，格式为username@host。\n下面命令生成一个4096位 RSA 加密算法的密钥对，并且给出了用户名和主机名。一般默认即可。\n1 $ ssh-keygen -t rsa -b 4096 -C \u0026#34;your_email@domain.com\u0026#34; （3）-f\n-f参数指定生成的私钥文件。\n1 $ ssh-keygen -t dsa -f mykey 上面命令会在当前目录生成私钥文件mykey和公钥文件mykey.pub。\n（4）-F\n-F参数检查某个主机名是否在known_hosts文件里面。\n1 $ ssh-keygen -F example.com （5）-N\n-N参数用于指定私钥的密码（passphrase）。\n1 $ ssh-keygen -t dsa -N secretword （6）-p\n-p参数用于重新指定私钥的密码（passphrase）。它与-N的不同之处在于，新密码不在命令中指定，而是执行后再输入。ssh 先要求输入旧密码，然后要求输入两遍新密码。\n（7）-R\n-R参数将指定的主机公钥指纹移出known_hosts文件。\n1 $ ssh-keygen -R example.com （8）-t\n-t参数用于指定生成密钥的加密算法，一般为dsa或rsa\n手动上传公钥 生成密钥以后，公钥必须上传到服务器，才能使用公钥登录。\nOpenSSH 规定，用户公钥保存在服务器的~/.ssh/authorized_keys文件。你要以哪个用户的身份登录到服务器，密钥就必须保存在该用户主目录的~/.ssh/authorized_keys文件。只要把公钥添加到这个文件之中，就相当于公钥上传到服务器了。每个公钥占据一行。如果该文件不存在，可以手动创建。\n用户可以手动编辑该文件，把公钥粘贴进去，也可以在本机计算机上，执行下面的命令：\n1 $ cat ~/.ssh/id_rsa.pub | ssh user@host \u0026#34;mkdir -p ~/.ssh \u0026amp;\u0026amp; cat \u0026gt;\u0026gt; ~/.ssh/authorized_keys\u0026#34; 上面示例中，user@host要替换成你所要登录的用户名和主机名。\n注意，authorized_keys文件的权限要设为644，即只有文件所有者才能写。如果权限设置不对，SSH 服务器可能会拒绝读取该文件：\n1 $ chmod 644 ~/.ssh/authorized_keys 只要公钥上传到服务器，下次登录时，OpenSSH 就会自动采用密钥登录，不再提示输入密码。\n1 2 3 4 $ ssh -l username shell.isp.com Enter passphrase for key \u0026#39;/home/you/.ssh/id_dsa\u0026#39;: ************ Last login: Mon Mar 24 02:17:27 2014 from ex.ample.com shell.isp.com\u0026gt; 上面例子中，SSH 客户端使用私钥之前，会要求用户输入密码（passphrase），用来解开私钥。如果秘钥有密码，那么每次都需要输入密码。\nssh-copy-id 命令：自动上传公钥 OpenSSH 自带一个ssh-copy-id命令，可以自动将公钥拷贝到远程服务器的~/.ssh/authorized_keys文件。如果~/.ssh/authorized_keys文件不存在，ssh-copy-id命令会自动创建该文件。\n用户在本地计算机执行下面的命令，就可以把本地的公钥拷贝到服务器。\n1 $ ssh-copy-id -i key_file user@host 上面命令中，-i参数用来指定公钥文件，user是所要登录的账户名，host是服务器地址。如果省略用户名，默认为当前的本机用户名。执行完该命令，公钥就会拷贝到服务器。\n注意，公钥文件可以不指定.pub后缀名，ssh-copy-id会自动在当前目录里面寻找。\n1 2 # ~/.ssh $ ssh-copy-id -i id_rsa user@host 上面命令中，公钥文件会自动匹配到~/.ssh/id_rsa.pub。\n注意，ssh-copy-id是直接将公钥添加到authorized_keys文件的末尾。如果authorized_keys文件的末尾不是一个换行符，会导致新的公钥添加到前一个公钥的末尾，两个公钥连在一起，使得它们都无法生效。所以，如果authorized_keys文件已经存在，使用ssh-copy-id命令之前，务必保证authorized_keys文件的末尾是换行符（假设该文件已经存在）。\nssh-agent 命令，ssh-add 命令 基本用法 私钥设置了密码以后，每次使用都必须输入密码，有时让人感觉非常麻烦。比如，连续使用scp命令远程拷贝文件时，每次都要求输入密码。\nssh-agent命令就是为了解决这个问题而设计的，它让用户在整个 Bash 对话（session）之中，只在第一次使用 SSH 命令时输入密码，然后将私钥保存在内存中，后面都不需要再输入私钥的密码了。\n第一步，使用下面的命令新建一次命令行对话:\n1 $ ssh-agent bash 上面命令中，如果你使用的命令行环境不是 Bash，可以用其他的 Shell 命令代替。比如zsh和fish。\n如果想在当前对话启用ssh-agent，可以使用下面的命令:\n1 $ eval `ssh-agent` 上面命令中，ssh-agent会先自动在后台运行，并将需要设置的环境变量输出在屏幕上，类似下面这样:\n1 2 3 4 $ ssh-agent SSH_AUTH_SOCK=/tmp/ssh-barrett/ssh-22841-agent; export SSH_AUTH_SOCK; SSH_AGENT_PID=22842; export SSH_AGENT_PID; echo Agent pid 22842; eval命令的作用，就是运行上面的ssh-agent命令的输出，设置环境变量。\n第二步，在新建的 Shell 对话里面，使用ssh-add命令添加默认的私钥（比如~/.ssh/id_rsa，或~/.ssh/id_dsa，或~/.ssh/id_ecdsa，或~/.ssh/id_ed25519）:\n1 2 3 $ ssh-add Enter passphrase for /home/you/.ssh/id_dsa: ******** Identity added: /home/you/.ssh/id_dsa (/home/you/.ssh/id_dsa) 上面例子中，添加私钥时，会要求输入密码。以后，在这个对话里面再使用密钥时，就不需要输入私钥的密码了，因为私钥已经加载到内存里面了。\n如果添加的不是默认私钥，ssh-add命令需要显式指定私钥文件:\n1 $ ssh-add my-other-key-file 上面的命令中，my-other-key-file就是用户指定的私钥文件。\n第三步，使用 ssh 命令正常登录远程服务器。\n1 $ ssh remoteHost 上面命令中，remoteHost是远程服务器的地址，ssh 使用的是默认的私钥。这时如果私钥设有密码，ssh 将不再询问密码，而是直接取出内存里面的私钥。\n如果要使用其他私钥登录服务器，需要使用 ssh 命令的-i参数指定私钥文件:\n1 $ ssh –i OpenSSHPrivateKey remoteHost 最后，如果要退出ssh-agent，可以直接退出子 Shell（按下 Ctrl + d），也可以使用下面的命令:\n1 $ ssh-agent -k ssh-add命令 ssh-add命令用来将私钥加入ssh-agent，它有如下的参数:\n（1）-d\n-d参数从内存中删除指定的私钥。\n1 $ ssh-add -d name-of-key-file （2）-D\n-D参数从内存中删除所有已经添加的私钥。\n1 $ ssh-add -D （3）-l\n-l参数列出所有已经添加的私钥。\n1 $ ssh-add -l 关闭密码登录 为了安全性，启用密钥登录之后，最好关闭服务器的密码登录。\n对于 OpenSSH，具体方法就是打开服务器 sshd 的配置文件/etc/ssh/sshd_config，将PasswordAuthentication这一项设为no。\n1 PasswordAuthentication no 修改配置文件以后，不要忘了重新启动 sshd，否则不会生效。\nSSH 证书登录 SSH 是服务器登录工具，一般情况下都采用密码登录或密钥登录。\n但是，SSH 还有第三种登录方法，那就是证书登录。某些情况下，它是更合理、更安全的登录方法，本文就介绍这种登录方法。\n非证书登录的缺点 密码登录和密钥登录，都有各自的缺点。\n密码登录需要输入服务器密码，这非常麻烦，也不安全，存在被暴力破解的风险。\n密钥登录需要服务器保存用户的公钥，也需要用户保存服务器公钥的指纹。这对于多用户、多服务器的大型机构很不方便，如果有员工离职，需要将他的公钥从每台服务器删除。\n证书登录是什么？ 证书登录就是为了解决上面的缺点而设计的。它引入了一个证书颁发机构（Certificate Authority，简称 CA），对信任的服务器颁发服务器证书，对信任的用户颁发用户证书。\n登录时，用户和服务器不需要提前知道彼此的公钥，只需要交换各自的证书，验证是否可信即可。\n证书登录的主要优点有两个：（1）用户和服务器不用交换公钥，这更容易管理，也具有更好的可扩展性。（2）证书可以设置到期时间，而公钥没有到期时间。针对不同的情况，可以设置有效期很短的证书，进一步提高安全性。\n证书登录的流程 SSH 证书登录之前，如果还没有证书，需要生成证书。具体方法是：（1）用户和服务器都将自己的公钥，发给 CA；（2）CA 使用服务器公钥，生成服务器证书，发给服务器；（3）CA 使用用户的公钥，生成用户证书，发给用户。\n有了证书以后，用户就可以登录服务器了。整个过程都是 SSH 自动处理，用户无感知。\n第一步，用户登录服务器时，SSH 自动将用户证书发给服务器。\n第二步，服务器检查用户证书是否有效，以及是否由可信的 CA 颁发。证实以后，就可以信任用户。\n第三步，SSH 自动将服务器证书发给用户。\n第四步，用户检查服务器证书是否有效，以及是否由信任的 CA 颁发。证实以后，就可以信任服务器。\n第五步，双方建立连接，服务器允许用户登录。\n生成 CA 的密钥 证书登录的前提是，必须有一个 CA，而 CA 本质上就是一对密钥，跟其他密钥没有不同，CA 就用这对密钥去签发证书。\n虽然 CA 可以用同一对密钥签发用户证书和服务器证书，但是出于安全性和灵活性，最好用不同的密钥分别签发。所以，CA 至少需要两对密钥，一对是签发用户证书的密钥，假设叫做user_ca，另一对是签发服务器证书的密钥，假设叫做host_ca。\n使用下面的命令，生成user_ca。\n1 2 # 生成 CA 签发用户证书的密钥 $ ssh-keygen -t rsa -b 4096 -f ~/.ssh/user_ca -C user_ca 上面的命令会在~/.ssh目录生成一对密钥：user_ca（私钥）和user_ca.pub（公钥）。\n这个命令的各个参数含义如下。\n-t rsa：指定密钥算法 RSA。 -b 4096：指定密钥的位数是4096位。安全性要求不高的场合，这个值可以小一点，但是不应小于1024。 -f ~/.ssh/user_ca：指定生成密钥的位置和文件名。 -C user_ca：指定密钥的识别字符串，相当于注释，可以随意设置。 使用下面的命令，生成host_ca。\n1 2 # 生成 CA 签发服务器证书的密钥 $ ssh-keygen -t rsa -b 4096 -f host_ca -C host_ca 上面的命令会在~/.ssh目录生成一对密钥：host_ca（私钥）和host_ca.pub（公钥）。\n现在，~/.ssh目录应该至少有四把密钥。\n~/.ssh/user_ca ~/.ssh/user_ca.pub ~/.ssh/host_ca ~/.ssh/host_ca.pub CA 签发服务器证书 有了秘钥以后，就可以签发服务器证书CA了。\n签发证书，除了 CA 的密钥以外，还需要服务器的公钥。一般来说，SSH 服务器（通常是sshd）安装时，已经生成密钥/etc/ssh/ssh_host_rsa_key了。如果没有的话，可以用下面的命令生成：\n1 $ sudo ssh-keygen -f /etc/ssh/ssh_host_rsa_key -b 4096 -t rsa 上面命令会在/etc/ssh目录，生成ssh_host_rsa_key（私钥）和ssh_host_rsa_key.pub（公钥）。然后，需要把服务器公钥ssh_host_rsa_key.pub，复制或上传到 CA 所在的服务器。\n上传以后，CA 就可以使用密钥host_ca为服务器的公钥ssh_host_rsa_key.pub签发服务器证书：\n1 $ ssh-keygen -s host_ca -I host.example.com -h -n host.example.com -V +52w ssh_host_rsa_key.pub 上面的命令会生成服务器证书ssh_host_rsa_key-cert.pub（服务器公钥名字加后缀-cert）。这个命令各个参数的含义如下。\n-s：指定 CA 签发证书的密钥。 -I：身份字符串，可以随便设置，相当于注释，方便区分证书，将来可以使用这个字符串撤销证书。 -h：指定该证书是服务器证书，而不是用户证书。 -n host.example.com：指定服务器的域名，表示证书仅对该域名有效。如果有多个域名，则使用逗号分隔。用户登录该域名服务器时，SSH 通过证书的这个值，分辨应该使用哪张证书发给用户，用来证明服务器的可信性。 -V +52w：指定证书的有效期，这里为52周（一年）。默认情况下，证书是永远有效的。建议使用该参数指定有效期，并且有效期最好短一点，最长不超过52周。 ssh_host_rsa_key.pub：服务器公钥。 生成证书以后，可以使用下面的命令，查看证书的细节。\n1 $ ssh-keygen -L -f ssh_host_rsa_key-cert.pub 最后，为证书设置权限。\n1 $ chmod 600 ssh_host_rsa_key-cert.pub CA 签发用户证书 下面，再用 CA 签发用户证书。这时需要用户的公钥，如果没有的话，客户端可以用下面的命令生成一对密钥：\n1 $ ssh-keygen -f ~/.ssh/user_key -b 4096 -t rsa 上面命令会在~/.ssh目录，生成user_key（私钥）和user_key.pub（公钥）。\n然后，将用户公钥user_key.pub，上传或复制到 CA 服务器。接下来，就可以使用 CA 的密钥user_ca为用户公钥user_key.pub签发用户证书：\n1 $ ssh-keygen -s user_ca -I user@example.com -n user -V +1d user_key.pub 上面的命令会生成用户证书user_key-cert.pub（用户公钥名字加后缀-cert）。这个命令各个参数的含义如下。\n-s：指定 CA 签发证书的密钥 -I：身份字符串，可以随便设置，相当于注释，方便区分证书，将来可以使用这个字符串撤销证书。 -n user：指定用户名，表示证书仅对该用户名有效。如果有多个用户名，使用逗号分隔。用户以该用户名登录服务器时，SSH 通过这个值，分辨应该使用哪张证书，证明自己的身份，发给服务器。 -V +1d：指定证书的有效期，这里为1天，强制用户每天都申请一次证书，提高安全性。默认情况下，证书是永远有效的。 user_key.pub：用户公钥。 生成证书以后，可以使用下面的命令，查看证书的细节。\n1 $ ssh-keygen -L -f user_key-cert.pub 最后，为证书设置权限。\n1 $ chmod 600 user_key-cert.pub 服务器安装证书 CA 生成服务器证书ssh_host_rsa_key-cert.pub以后，需要将该证书发回服务器，可以使用下面的scp命令，将证书拷贝过去：\n1 $ scp ~/.ssh/ssh_host_rsa_key-cert.pub root@host.example.com:/etc/ssh/ 然后，将下面一行添加到服务器配置文件/etc/ssh/sshd_config：\n1 HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub 上面的代码告诉 sshd，服务器证书是哪一个文件。\n重新启动 sshd。\n1 2 3 $ sudo systemctl restart sshd # 或者 $ sudo service sshd restart 服务器安装 CA 公钥 为了让服务器信任用户证书，必须将 CA 签发用户证书的公钥user_ca.pub，拷贝到服务器：\n1 $ scp ~/.ssh/user_ca.pub root@host.example.com:/etc/ssh/ 上面的命令，将 CA 签发用户证书的公钥user_ca.pub，拷贝到 SSH 服务器的/etc/ssh目录。\n然后，将下面一行添加到服务器配置文件/etc/ssh/sshd_config：\n1 TrustedUserCAKeys /etc/ssh/user_ca.pub 上面的做法是将user_ca.pub加到/etc/ssh/sshd_config，这会产生全局效果，即服务器的所有账户都会信任user_ca签发的所有用户证书。\n另一种做法是将user_ca.pub加到服务器某个账户的~/.ssh/authorized_keys文件，只让该账户信任user_ca签发的用户证书。具体方法是打开~/.ssh/authorized_keys，追加一行，开头是@cert-authority principals=\u0026quot;...\u0026quot;，然后后面加上user_ca.pub的内容，大概是下面这个样子。\n1 @cert-authority principals=\u0026#34;user\u0026#34; ssh-rsa AAAAB3Nz...XNRM1EX2gQ== 上面代码中，principals=\u0026quot;user\u0026quot;指定用户登录的服务器账户名，一般就是authorized_keys文件所在的账户。\n重新启动 sshd。\n1 2 3 $ sudo systemctl restart sshd # 或者 $ sudo service sshd restart 至此，SSH 服务器已配置为信任user_ca签发的证书。\n客户端安装证书 客户端安装用户证书很简单，就是从 CA 将用户证书user_key-cert.pub复制到客户端，与用户的密钥user_key保存在同一个目录即可。\n客户端安装 CA 公钥 为了让客户端信任服务器证书，必须将 CA 签发服务器证书的公钥host_ca.pub，加到客户端的/etc/ssh/ssh_known_hosts文件（全局级别）或者~/.ssh/known_hosts文件（用户级别）。\n具体做法是打开ssh_known_hosts或known_hosts文件，追加一行，开头为@cert-authority *.example.com，然后将host_ca.pub文件的内容（即公钥）粘贴在后面，大概是下面这个样子：\n1 @cert-authority *.example.com ssh-rsa AAAAB3Nz...XNRM1EX2gQ== 上面代码中，*.example.com是域名的模式匹配，表示只要服务器符合该模式的域名，且签发服务器证书的 CA 匹配后面给出的公钥，就都可以信任。如果没有域名限制，这里可以写成*。如果有多个域名模式，可以使用逗号分隔；如果服务器没有域名，可以用主机名（比如host1,host2,host3）或者 IP 地址（比如11.12.13.14,21.22.23.24）。\n然后，就可以使用证书，登录远程服务器了。\n1 $ ssh -i ~/.ssh/user_key user@host.example.com 上面命令的-i参数用来指定用户的密钥。如果证书与密钥在同一个目录，则连接服务器时将自动使用该证书。\n废除证书 废除证书的操作，分成用户证书的废除和服务器证书的废除两种。\n服务器证书的废除，用户需要在known_hosts文件里面，修改或删除对应的@cert-authority命令的那一行。\n用户证书的废除，需要在服务器新建一个/etc/ssh/revoked_keys文件，然后在配置文件sshd_config添加一行，内容如下：\n1 RevokedKeys /etc/ssh/revoked_keys revoked_keys文件保存不再信任的用户公钥，由下面的命令生成：\n1 $ ssh-keygen -kf /etc/ssh/revoked_keys -z 1 ~/.ssh/user1_key.pub 上面命令中，-z参数用来指定用户公钥保存在revoked_keys文件的哪一行，这个例子是保存在第1行。\n如果以后需要废除其他的用户公钥，可以用下面的命令保存在第2行。\n1 $ ssh-keygen -ukf /etc/ssh/revoked_keys -z 2 ~/.ssh/user2_key.pub 参考链接 SSH Emergency Access, Carl Tashian Using OpenSSH Certificate Authentication, Red Hat Enterprise Linux Deployment Guide How to SSH Properly, Gus Luxton scp 命令 scp是 SSH 提供的一个客户端程序，用来在两台主机之间加密传送文件（即复制文件）。\n简介 scp是 secure copy 的缩写，相当于cp命令 + SSH。它的底层是 SSH 协议，默认端口是22，相当于先使用ssh命令登录远程主机，然后再执行拷贝操作。\nscp主要用于以下三种复制操作：\n本地复制到远程。 远程复制到本地。 两个远程系统之间的复制。 使用scp传输数据时，文件和密码都是加密的，不会泄漏敏感信息。\n基本语法 scp的语法类似cp的语法：\n1 $ scp source destination 上面命令中，source是文件当前的位置，destination是文件所要复制到的位置。它们都可以包含用户名和主机名。\n1 $ scp user@host:foo.txt bar.txt 上面命令将远程主机（user@host）用户主目录下的foo.txt，复制为本机当前目录的bar.txt。可以看到，主机与文件之间要使用冒号（:）分隔。\nscp会先用 SSH 登录到远程主机，然后在加密连接之中复制文件。客户端发起连接后，会提示用户输入密码，这部分是跟 SSH 的用法一致的。\n用户名和主机名都是可以省略的。用户名的默认值是本机的当前用户名，主机名默认为当前主机。注意，scp会使用 SSH 客户端的配置文件.ssh/config，如果配置文件里面定义了主机的别名，这里也可以使用别名连接。\nscp支持一次复制多个文件:\n1 $ scp source1 source2 destination 上面命令会将source1和source2两个文件，复制到destination。\n注意，如果所要复制的文件，在目标位置已经存在同名文件，scp会在没有警告的情况下覆盖同名文件。\n用法示例 （1）本地文件复制到远程\n复制本机文件到远程系统的用法如下:\n1 2 3 4 5 # 语法 $ scp SourceFile user@host:directory/TargetFile # 示例 $ scp file.txt remote_username@10.10.0.2:/remote/directory 下面是复制整个目录的例子:\n1 2 3 4 5 6 7 8 9 # 将本机的 documents 目录拷贝到远程主机， # 会在远程主机创建 documents 目录: 远程目录存在，则复制，不存在则改名。注意只能一级目录 $ scp -r documents username@server_ip:/path_to_remote_directory # 将本机整个目录拷贝到远程目录下 $ scp -r localmachine/path_to_the_directory username@server_ip:/path_to_remote_directory/ # 将本机目录下的所有内容拷贝到远程目录下 $ scp -r localmachine/path_to_the_directory/* username@server_ip:/path_to_remote_directory/ （2）远程文件复制到本地\n从远程主机复制文件到本地的用法如下：\n1 2 3 4 5 # 语法 $ scp user@host:directory/SourceFile TargetFile # 示例 $ scp remote_username@10.10.0.2:/remote/file.txt /local/directory 下面是复制整个目录的例子：\n1 2 3 4 5 6 # 拷贝一个远程目录到本机目录下 $ scp -r username@server_ip:/path_to_remote_directory local-machine/path_to_the_directory/ # 拷贝远程目录下的所有内容，到本机目录下 $ scp -r username@server_ip:/path_to_remote_directory/* local-machine/path_to_the_directory/ $ scp -r user@host:directory/SourceFolder TargetFolder （3）两个远程系统之间的复制\n本机发出指令，从远程主机 A 拷贝到远程主机 B 的用法如下：\n1 2 3 4 5 # 语法 $ scp user@host1:directory/SourceFile user@host2:directory/SourceFile # 示例 $ scp user1@host1.com:/files/file.txt user2@host2.com:/files 系统将提示你输入两个远程帐户的密码。数据将直接从一个远程主机传输到另一个远程主机。\n注意：远程目录存在，则复制，不存在则改名。注意只能改一级目录。\n配置项 （1）-c\n-c参数用来指定文件拷贝数据传输的加密算法。\n1 $ scp -c blowfish some_file your_username@remotehost.edu:~ 上面代码指定加密算法为blowfish。\n（2）-C\n-C参数表示是否在传输时压缩文件。\n1 $ scp -c blowfish -C local_file your_username@remotehost.edu:~ （3）-F\n-F参数用来指定 ssh_config 文件，供 ssh 使用。\n1 $ scp -F /home/pungki/proxy_ssh_config Label.pdf root@172.20.10.8:/root （4）-i\n-i参数用来指定密钥。\n1 $ scp -vCq -i private_key.pem ~/test.txt root@192.168.1.3:/some/path/test.txt （5）-l\n-l参数用来限制传输数据的带宽速率，单位是 Kbit/sec。对于多人分享的带宽，这个参数可以留出一部分带宽供其他人使用。\n1 $ scp -l 80 yourusername@yourserver:/home/yourusername/* . 上面代码中，scp命令占用的带宽限制为每秒 80K 比特位，即每秒 10K 字节。\n（6）-p\n-p参数用来保留修改时间（modification time）、访问时间（access time）、文件状态（mode）等原始文件的信息。\n1 $ scp -p ~/test.txt root@192.168.1.3:/some/path/test.txt （7）-P\n-P参数用来指定远程主机的 SSH 端口。如果远程主机使用默认端口22，可以不用指定，否则需要用-P参数在命令中指定。\n1 $ scp -P 2222 user@host:directory/SourceFile TargetFile （8）-q\n-q参数用来关闭显示拷贝的进度条。\n1 $ scp -q Label.pdf mrarianto@202.x.x.x:. （9）-r\n-r参数表示是否以递归方式复制目录。\n（10）-v\n-v参数用来显示详细的输出。\n1 $ scp -v ~/test.txt root@192.168.1.3:/root/help2356.txt sftp 命令 FTP（File Transfer Protocol，文件传输协议），提供文件的上传和下载功能。sftp是基于SSH协议的文件传输协议，提供了加密和认证功能。\nsftp是 SSH 提供的一个客户端应用程序（意味着认证方式通用），主要用来安全地访问 FTP。因为 FTP 是不加密协议，很不安全，sftp就相当于将 FTP 放入了 SSH。\n下面的命令连接 FTP 主机：\n1 $ sftp username@hostname 执行上面的命令，会要求输入 FTP 的密码。密码验证成功以后，就会出现 FTP 的提示符sftp\u0026gt; ，下面是一个例子：\n1 2 3 4 $ sftp USER@penguin.example.com USER@penguin.example.com\u0026#39;s password: Connected to penguin.example.com. sftp\u0026gt; FTP 的提示符下面，就可以输入各种 FTP 命令了，这部分完全跟传统的 FTP 用法完全一样：\nls [directory]：列出一个远程目录的内容。如果没有指定目标目录，则默认列出当前目录。\ncd directory：从当前目录改到指定目录。\nmkdir directory：创建一个远程目录。\nrmdir path：删除一个远程目录。\nput localfile [remotefile]：本地文件传输到远程主机。\n需要注意的是远程主机的工作目录是当前用户的主目录。本地主机是当前目录。 所以可以相互省略工作目录 put [filename]：上传文件。 get remotefile [localfile]：远程文件传输到本地。\n这里可以省略本地主机当前目录，默认为本地主机当前目录。 目录加-r选项。 get [filename]：下载文件。 bye：退出 sftp。\nquit：退出 sftp。\nexit：退出 sftp。\nhelp：显示帮助信息。\n更多命令基本与Linux通用。 rsync 命令 简介 rsync 是一个常用的 Linux 应用程序，用于文件同步。\n它可以在本地计算机与远程计算机之间，或者两个本地目录之间同步文件（但不支持两台远程计算机之间的同步）。它也可以当作文件复制工具，替代cp和mv命令。与scp类似。\n它名称里面的r指的是 remote，rsync 其实就是“远程同步”（remote sync）的意思。与其他文件传输工具（如 FTP 或 scp）不同，rsync 的最大特点是会检查发送方和接收方已有的文件，仅传输有变动的部分（默认规则是文件大小或修改时间有变动）。\n虽然 rsync 不是 SSH 工具集的一部分，但因为也涉及到远程操作，所以放在这里一起介绍。\nrsync具有以下主要特性：\n增量传输：只传输源和目标之间的差异，而不是整个文件或目录。 快速：通过采用一种叫做“快速检查算法”（rolling checksum）的方法，它可以快速找到文件的差异。 灵活：支持各种不同的传输模式，包括本地到本地、本地到远程、远程到本地。 安全：可以通过SSH协议传输数据，保证数据的安全性。 支持软链接、硬链接和设备文件：在同步过程中，能够保留这些文件的属性。 可断点续传：在传输中断后，能够从中断点继续。 文件权限和所有权保留：能够保留文件的权限、所有者和时间戳信息。 广泛的选项和参数：提供了丰富的选项和参数来满足各种需求。 安装 如果本机或者远程计算机没有安装 rsync，可以用下面的命令安装:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # Debian/Ubuntu $ sudo apt install rsync # CentOS/RHEL $ sudo yum install rsync # CentOS 8及之后的版本(包括RHEL 8)/Fedora $ sudo dnf install rsync # Arch Linux/Windows msys2 $ sudo pacman -S rsync # macOS $ brew install rsync # FreeBSD sudo pkg install rsync 注意，传输的双方都必须安装 rsync。\n基本用法 rsync的基本语法如下：\n1 rsync [options] source destination source：源文件或目录的路径。\ndestination：目标文件或目录的路径。\n远程主机在路径前加usname@hostname:，与scp一样。\nrsync 可以用于本地计算机的两个目录之间的同步。下面就用本地同步举例，顺便讲解 rsync 几个主要参数的用法:\n-v：显示详细的输出信息。\n-r参数\n本机使用 rsync 命令时，可以作为cp和mv命令的替代方法，将源目录拷贝到目标目录:\n1 $ rsync -r source destination 上面命令中，-r表示递归，即包含子目录。注意，-r是必须的，否则 rsync 运行不会成功。source目录表示源目录，destination表示目标目录。上面命令执行以后，目标目录下就会出现destination/source这个子目录。\n如果有多个文件或目录需要同步，可以写成下面这样:\n1 $ rsync -r source1 source2 destination 上面命令中，source1、source2都会被同步到destination目录。\n-a参数\n-a参数可以替代-r，除了可以递归同步以外，还可以同步元信息（比如修改时间、权限等）。由于 rsync 默认使用文件大小和修改时间决定文件是否需要更新，所以-a比-r更有用。下面的用法才是常见的写法:\n1 $ rsync -a source destination 目标目录destination如果不存在，rsync 会自动创建。执行上面的命令后，源目录source被完整地复制到了目标目录destination下面，即形成了destination/source的目录结构。\n如果只想同步源目录source里面的内容到目标目录destination，则需要在源目录后面加上斜杠:\n1 $ rsync -a source/ destination 上面命令执行后，source目录里面的内容，就都被复制到了destination目录里面，并不会在destination下面创建一个source子目录。\n-n参数\n如果不确定 rsync 执行后会产生什么结果，可以先用-n或--dry-run参数模拟执行的结果：\n1 $ rsync -anv source/ destination 上面命令中，-n参数模拟命令执行的结果，并不真的执行命令。-v参数则是将结果输出到终端，这样就可以看到哪些内容会被同步。\n--delete参数\n默认情况下，rsync 只确保源目录的所有内容（明确排除的文件除外）都复制到目标目录。它不会使两个目录保持相同，并且不会删除文件。如果要使得目标目录成为源目录的镜像副本，则必须使用--delete参数，这将删除只存在于目标目录、不存在于源目录的文件。\n1 $ rsync -av --delete source/ destination 上面命令中，--delete参数会使得destination成为source的一个镜像。\n排除文件 --exclude参数\n有时，我们希望同步时排除某些文件或目录，这时可以用--exclude参数指定排除模式：\n1 2 3 $ rsync -av --exclude=\u0026#39;*.txt\u0026#39; source/ destination # 或者 $ rsync -av --exclude \u0026#39;*.txt\u0026#39; source/ destination 上面命令排除了所有 TXT 文件。\n注意，rsync 会同步以“点”开头的隐藏文件，如果要排除隐藏文件，可以这样写--exclude=\u0026quot;.*\u0026quot;。\n如果要排除某个目录里面的所有文件，但不希望排除目录本身，可以写成下面这样：\n1 $ rsync -av --exclude \u0026#39;dir1/*\u0026#39; source/ destination 多个排除模式，可以用多个--exclude参数：\n1 $ rsync -av --exclude \u0026#39;file1.txt\u0026#39; --exclude \u0026#39;dir1/*\u0026#39; source/ destination 多个排除模式也可以利用 Bash 的大扩号的扩展功能，只用一个--exclude参数：\n1 $ rsync -av --exclude={\u0026#39;file1.txt\u0026#39;,\u0026#39;dir1/*\u0026#39;} source/ destination 如果排除模式很多，可以将它们写入一个文件，每个模式一行，然后用--exclude-from参数指定这个文件：\n1 $ rsync -av --exclude-from=\u0026#39;exclude-file.txt\u0026#39; source/ destination --include参数\n--include参数用来指定必须同步的文件模式，往往与--exclude结合使用：\n1 $ rsync -av --include=\u0026#34;*.txt\u0026#34; --exclude=\u0026#39;*\u0026#39; source/ destination 上面命令指定同步时，排除所有文件，但是会包括 TXT 文件。\n远程同步 SSH 协议 rsync 除了支持本地两个目录之间的同步，也支持远程同步。它可以将本地内容，同步到远程服务器：\n1 $ rsync -av source/ username@remote_host:destination 也可以将远程内容同步到本地：\n1 $ rsync -av username@remote_host:source/ destination rsync 默认使用 SSH 进行远程登录和数据传输。\n由于早期 rsync 不使用 SSH 协议，需要用-e参数指定协议，后来才改的。所以，下面-e ssh可以省略：\n1 $ rsync -av -e ssh source/ user@remote_host:/destination 但是，如果 ssh 命令有附加的参数，则必须使用-e参数指定所要执行的 SSH 命令：\n1 $ rsync -av -e \u0026#39;ssh -p 2234\u0026#39; source/ user@remote_host:/destination 上面命令中，-e参数指定 SSH 使用2234端口。\nrsync 协议 除了使用 SSH，如果另一台服务器安装并运行了 rsync 守护程序，则也可以用rsync://协议（默认端口873）进行传输。具体写法是服务器与目标目录之间使用双冒号分隔::：\n1 $ rsync -av source/ 192.168.122.32::module/destination 注意，上面地址中的module并不是实际路径名，而是 rsync 守护程序指定的一个资源名，由管理员分配。\n如果想知道 rsync 守护程序分配的所有 module 列表，可以执行下面命令：\n1 $ rsync rsync://192.168.122.32 rsync 协议除了使用双冒号，也可以直接用rsync://协议指定地址：\n1 $ rsync -av source/ rsync://192.168.122.32/module/destination 增量备份 rsync 的最大特点就是它可以完成增量备份，也就是默认只复制有变动的文件。\n除了源目录与目标目录直接比较，rsync 还支持使用基准目录，即将源目录与基准目录之间变动的部分，同步到目标目录。\n具体做法是，第一次同步是全量备份，所有文件在基准目录里面同步一份。以后每一次同步都是增量备份，只同步源目录与基准目录之间有变动的部分，将这部分保存在一个新的目标目录。这个新的目标目录之中，也是包含所有文件，但实际上，只有那些变动过的文件是存在于该目录，其他没有变动的文件都是指向基准目录文件的硬链接。\n--link-dest参数用来指定同步时的基准目录：\n1 $ rsync -a --delete --link-dest /compare/path /source/path /target/path 上面命令中，--link-dest参数指定基准目录/compare/path，然后源目录/source/path跟基准目录进行比较，找出变动的文件，将它们拷贝到目标目录/target/path。那些没变动的文件则会生成硬链接。这个命令的第一次备份时是全量备份，后面就都是增量备份了。\n下面是一个脚本示例，备份用户的主目录：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/bash # A script to perform incremental backups using rsync set -o errexit set -o nounset set -o pipefail readonly SOURCE_DIR=\u0026#34;${HOME}\u0026#34; readonly BACKUP_DIR=\u0026#34;/mnt/data/backups\u0026#34; readonly DATETIME=\u0026#34;$(date \u0026#39;+%Y-%m-%d_%H:%M:%S\u0026#39;)\u0026#34; readonly BACKUP_PATH=\u0026#34;${BACKUP_DIR}/${DATETIME}\u0026#34; readonly LATEST_LINK=\u0026#34;${BACKUP_DIR}/latest\u0026#34; mkdir -p \u0026#34;${BACKUP_DIR}\u0026#34; rsync -av --delete \\ \u0026#34;${SOURCE_DIR}/\u0026#34; \\ --link-dest \u0026#34;${LATEST_LINK}\u0026#34; \\ --exclude=\u0026#34;.cache\u0026#34; \\ \u0026#34;${BACKUP_PATH}\u0026#34; rm -rf \u0026#34;${LATEST_LINK}\u0026#34; ln -s \u0026#34;${BACKUP_PATH}\u0026#34; \u0026#34;${LATEST_LINK}\u0026#34; 上面脚本中，每一次同步都会生成一个新目录${BACKUP_DIR}/${DATETIME}，并将软链接${BACKUP_DIR}/latest指向这个目录。下一次备份时，就将${BACKUP_DIR}/latest作为基准目录，生成新的备份目录。最后，再将软链接${BACKUP_DIR}/latest指向新的备份目录。\n配置项 -a、--archive参数表示存档模式，保存所有的元数据，比如修改时间（modification time）、权限、所有者等，并且软链接也会同步过去。\n--append参数指定文件接着上次中断的地方，继续传输。\n--append-verify参数跟--append参数类似，但会对传输完成后的文件进行一次校验。如果校验失败，将重新发送整个文件。\n-b、--backup参数指定在删除或更新目标目录已经存在的文件时，将该文件更名后进行备份，默认行为是删除。更名规则是添加由--suffix参数指定的文件后缀名，默认是~。\n--backup-dir参数指定文件备份时存放的目录，比如--backup-dir=/path/to/backups。\n--bwlimit参数指定带宽限制，默认单位是 KB/s，比如--bwlimit=100。\n-c、--checksum参数改变rsync的校验方式。默认情况下，rsync 只检查文件的大小和最后修改日期是否发生变化，如果发生变化，就重新传输；使用这个参数以后，则通过判断文件内容的校验和，决定是否重新传输。\n--delete参数删除只存在于目标目录、不存在于源目标的文件，即保证目标目录是源目标的镜像。\n-e参数指定使用 SSH 协议传输数据。\n--exclude参数指定排除不进行同步的文件，比如--exclude=\u0026quot;*.iso\u0026quot;。\n--exclude-from参数指定一个本地文件，里面是需要排除的文件模式，每个模式一行。\n--existing、--ignore-non-existing参数表示不同步目标目录中不存在的文件和目录。\n-h参数表示以人类可读的格式输出。\n-h、--help参数返回帮助信息。\n-i参数表示输出源目录与目标目录之间文件差异的详细情况。\n--ignore-existing参数表示只要该文件在目标目录中已经存在，就跳过去，不再同步这些文件。\n--include参数指定同步时要包括的文件，一般与--exclude结合使用。\n--link-dest参数指定增量备份的基准目录。\n-m参数指定不同步空目录。\n--max-size参数设置传输的最大文件的大小限制，比如不超过200KB（--max-size='200k'）。\n--min-size参数设置传输的最小文件的大小限制，比如不小于10KB（--min-size=10k）。\n-n参数或--dry-run参数模拟将要执行的操作，而并不真的执行。配合-v参数使用，可以看到哪些内容会被同步过去。\n-P参数是--progress和--partial这两个参数的结合。\n--partial参数允许恢复中断的传输。不使用该参数时，rsync会删除传输到一半被打断的文件；使用该参数后，传输到一半的文件也会同步到目标目录，下次同步时再恢复中断的传输。一般需要与--append或--append-verify配合使用。\n--partial-dir参数指定将传输到一半的文件保存到一个临时目录，比如--partial-dir=.rsync-partial。一般需要与--append或--append-verify配合使用。\n--progress参数表示显示进展。\n-r参数表示递归，即包含子目录。\n--remove-source-files参数表示传输成功后，删除发送方的文件。\n--size-only参数表示只同步大小有变化的文件，不考虑文件修改时间的差异。\n--suffix参数指定文件名备份时，对文件名添加的后缀，默认是~。\n-u、--update参数表示同步时跳过目标目录中修改时间更新的文件，即不同步这些有更新的时间戳的文件。\n-v参数表示输出细节。-vv表示输出更详细的信息，-vvv表示输出最详细的信息。\n--version参数返回 rsync 的版本。\n-z参数指定同步时压缩数据。\n参考链接 How To Use Rsync to Sync Local and Remote Directories on a VPS, Justin Ellingwood Mirror Your Web Site With rsync, Falko Timme Examples on how to use Rsync, Egidio Docile How to create incremental backups using rsync on Linux, Egidio Docile SSH 服务器 简介 SSH 的架构是服务器/客户端模式，两端运行的软件是不一样的。OpenSSH 的客户端软件是 ssh，服务器软件是 sshd。本章介绍 sshd 的各种知识。\n如果没有安装 sshd，可以用下面的命令安装:\n在Linux上安装SSH服务端\n1 2 3 4 5 6 7 8 # Debian/Ubuntu $ sudo apt install openssh-server # CentOS/RHEL $ sudo yum install openssh-server # Fedora $ sudo dnf install openssh-server 在macOS上安装SSH服务端\nmacOS内置了OpenSSH服务端，但默认情况下它是关闭的。可以通过以下步骤启用：\n打开“系统偏好设置”。 进入“共享”设置。 勾选“远程登录”选项。 选择允许哪些用户可以通过SSH登录。 之后，SSH服务端将开始运行，你可以使用SSH客户端连接到你的macOS机器。\n在Windows上安装SSH服务端\n从Windows 10版本1809开始，Windows内置了OpenSSH Server。可以通过以下步骤启用：\n打开“设置” \u0026gt; “应用” \u0026gt; “可选功能”。\n向下滚动并点击“添加功能”。\n在列表中找到并安装“OpenSSH Server”。\n安装完成后，打开PowerShell并启动SSH服务：\n1 Start-Service sshd 使SSH服务在开机时自动启动：\n1 Set-Service -Name sshd -StartupType \u0026#39;Automatic\u0026#39; 如果需要检查服务状态：\n1 Get-Service -Name sshd 一般来说，sshd 安装后会跟着系统一起启动。如果当前 sshd 没有启动，可以用下面的命令启动。\n1 $ sshd 上面的命令运行后，如果提示“sshd re-exec requires execution with an absolute path”，就需要使用绝对路径来启动。这是为了防止有人出于各种目的，放置同名软件在$PATH变量指向的目录中，代替真正的 sshd:\n1 2 # Centos、Ubuntu、OS X $ /usr/sbin/sshd 上面的命令运行以后，sshd 自动进入后台，所以命令后面不需要加上\u0026amp;。\n除了直接运行可执行文件，也可以通过 Systemd 启动 sshd:\n启动SSH服务并使其在开机时自动启动：\n1 2 3 4 5 6 7 # Debian/Ubuntu sudo systemctl start ssh sudo systemctl enable ssh # CentOS/RHEL/Fedora sudo systemctl start sshd sudo systemctl enable sshd 检查SSH服务的状态：\n1 2 3 4 5 # Debian/Ubuntu sudo systemctl status ssh # CentOS/RHEL/Fedora sudo systemctl status sshd 重启SSH服务：\n1 2 3 4 5 # Debian/Ubuntu $ sudo systemctl restart ssh # CentOS/RHEL/Fedora $ sudo systemctl restart sshd sshd 配置文件 sshd 的配置文件在/etc/ssh目录，主配置文件是sshd_config，此外还有一些安装时生成的密钥：\n/etc/ssh/sshd_config：配置文件 /etc/ssh/ssh_host_ecdsa_key：ECDSA 私钥。 /etc/ssh/ssh_host_ecdsa_key.pub：ECDSA 公钥。 /etc/ssh/ssh_host_key：用于 SSH 1 协议版本的 RSA 私钥。 /etc/ssh/ssh_host_key.pub：用于 SSH 1 协议版本的 RSA 公钥。 /etc/ssh/ssh_host_rsa_key：用于 SSH 2 协议版本的 RSA 私钥。 /etc/ssh/ssh_host_rsa_key.pub：用于 SSH 2 协议版本的 RSA 公钥。 /etc/pam.d/sshd：PAM 配置文件。 注意，如果重装 sshd，上面这些密钥都会重新生成，导致客户端重新连接 ssh 服务器时，会跳出警告，拒绝连接。为了避免这种情况，可以在重装 sshd 时，先备份/etc/ssh目录，重装后再恢复这个目录。\n配置文件sshd_config的格式是，每个命令占据一行。每行都是配置项和对应的值，配置项的大小写不敏感，与值之间使用空格分隔。\n1 Port 2034 上面的配置命令指定，配置项Port的值是2034。Port写成port也可。\n配置文件还有另一种格式，就是配置项与值之间有一个等号，等号前后的空格可选。\n1 Port = 2034 配置文件里面，#开头的行表示注释。\n1 # 这是一行注释 注意，注释只能放在一行的开头，不能放在一行的结尾。\n1 Port 2034 # 此处不允许注释 上面的写法是错误的。\n另外，空行等同于注释。\nsshd 启动时会自动读取默认的配置文件。如果希望使用其他的配置文件，可以用 sshd 命令的-f参数指定。\n1 $ sshd -f /usr/local/ssh/my_config 上面的命令指定 sshd 使用另一个配置文件my_config。\n修改配置文件以后，可以用 sshd 命令的-t（test）检查有没有语法错误。\n1 $ sshd -t 配置文件修改以后，并不会自动生效，必须重新启动 sshd。\n1 $ sudo systemctl restart sshd sshd 密钥 sshd 有自己的一对或多对密钥。它使用密钥向客户端证明自己的身份。所有密钥都是公钥和私钥成对出现，公钥的文件名一般是私钥文件名加上后缀.pub。\nDSA 格式的密钥文件默认为/etc/ssh/ssh_host_dsa_key（公钥为ssh_host_dsa_key.pub），RSA 格式的密钥为/etc/ssh/ssh_host_rsa_key（公钥为ssh_host_rsa_key.pub）。如果需要支持 SSH 1 协议，则必须有密钥/etc/ssh/ssh_host_key。\n如果密钥不是默认文件，那么可以通过配置文件sshd_config的HostKey配置项指定。默认密钥的HostKey设置如下：\n1 2 3 4 5 6 # HostKey for protocol version 1 # HostKey /etc/ssh/ssh_host_key # HostKeys for protocol version 2 # HostKey /etc/ssh/ssh_host_rsa_key # HostKey /etc/ssh/ssh_host_dsa_key 上面命令前面的#表示这些行都是注释，因为这是默认值，有没有这几行都一样。\n如果要修改密钥，就要去掉行首的#，指定其他密钥。\n1 2 3 HostKey /usr/local/ssh/my_dsa_key HostKey /usr/local/ssh/my_rsa_key HostKey /usr/local/ssh/my_old_ssh1_key sshd 配置项 以下是/etc/ssh/sshd_config文件里面的配置项：\nAcceptEnv\nAcceptEnv指定允许接受客户端通过SendEnv命令发来的哪些环境变量，即允许客户端设置服务器的环境变量清单，变量名之间使用空格分隔（AcceptEnv PATH TERM）。\nAllowGroups\nAllowGroups指定允许登录的用户组（AllowGroups groupName，多个组之间用空格分隔。如果不使用该项，则允许所有用户组登录。\nAllowUsers\nAllowUsers指定允许登录的用户，用户名之间使用空格分隔（AllowUsers user1 user2），也可以使用多行AllowUsers命令指定，用户名支持使用通配符。如果不使用该项，则允许所有用户登录。该项也可以使用用户名@域名的格式（比如AllowUsers jones@example.com）。\nAllowTcpForwarding\nAllowTcpForwarding指定是否允许端口转发，默认值为yes（AllowTcpForwarding yes），local表示只允许本地端口转发，remote表示只允许远程端口转发。\nAuthorizedKeysFile\nAuthorizedKeysFile指定储存用户公钥的目录，默认是用户主目录的ssh/authorized_keys目录（AuthorizedKeysFile .ssh/authorized_keys）。\nBanner\nBanner指定用户登录后，sshd 向其展示的信息文件（Banner /usr/local/etc/warning.txt），默认不展示任何内容。\nChallengeResponseAuthentication\nChallengeResponseAuthentication指定是否使用“键盘交互”身份验证方案，默认值为yes（ChallengeResponseAuthentication yes）。\n从理论上讲，“键盘交互”身份验证方案可以向用户询问多重问题，但是实践中，通常仅询问用户密码。如果要完全禁用基于密码的身份验证，请将PasswordAuthentication和ChallengeResponseAuthentication都设置为no。\nCiphers\nCiphers指定 sshd 可以接受的加密算法（Ciphers 3des-cbc），多个算法之间使用逗号分隔。\nClientAliveCountMax\nClientAliveCountMax指定建立连接后，客户端失去响应时，服务器尝试连接的次数（ClientAliveCountMax 8）。\nClientAliveInterval\nClientAliveInterval指定允许客户端发呆的时间，单位为秒（ClientAliveInterval 180）。如果这段时间里面，客户端没有发送任何信号，SSH 连接将关闭。\nCompression\nCompression指定客户端与服务器之间的数据传输是否压缩。默认值为yes（Compression yes）\nDenyGroups\nDenyGroups指定不允许登录的用户组（DenyGroups groupName）。\nDenyUsers\nDenyUsers指定不允许登录的用户（DenyUsers user1），用户名之间使用空格分隔，也可以使用多行DenyUsers命令指定。\nFascistLogging\nSSH 1 版本专用，指定日志输出全部 Debug 信息（FascistLogging yes）。\nHostKey\nHostKey指定 sshd 服务器的密钥，详见前文。\nKeyRegenerationInterval\nKeyRegenerationInterval指定 SSH 1 版本的密钥重新生成时间间隔，单位为秒，默认是3600秒（KeyRegenerationInterval 3600）。\nListenAddress\nListenAddress指定 sshd 监听的本机 IP 地址，即 sshd 启用的 IP 地址，默认是 0.0.0.0（ListenAddress 0.0.0.0）表示在本机所有网络接口启用。可以改成只在某个网络接口启用（比如ListenAddress 192.168.10.23），也可以指定某个域名启用（比如ListenAddress server.example.com）。\n如果要监听多个指定的 IP 地址，可以使用多行ListenAddress命令。\n1 2 ListenAddress 172.16.1.1 ListenAddress 192.168.0.1 LoginGraceTime\nLoginGraceTime指定允许客户端登录时发呆的最长时间，比如用户迟迟不输入密码，连接就会自动断开，单位为秒（LoginGraceTime 60）。如果设为0，就表示没有限制。\nLogLevel\nLogLevel指定日志的详细程度，可能的值依次为QUIET、FATAL、ERROR、INFO、VERBOSE、DEBUG、DEBUG1、DEBUG2、DEBUG3，默认为INFO（LogLevel INFO）。\nMACs\nMACs指定sshd 可以接受的数据校验算法（MACs hmac-sha1），多个算法之间使用逗号分隔。\nMaxAuthTries\nMaxAuthTries指定允许 SSH 登录的最大尝试次数（MaxAuthTries 3），如果密码输入错误达到指定次数，SSH 连接将关闭。\nMaxStartups\nMaxStartups指定允许同时并发的 SSH 连接数量（MaxStartups）。如果设为0，就表示没有限制。\n这个属性也可以设为A:B:C的形式，比如MaxStartups 10:50:20，表示如果达到10个并发连接，后面的连接将有50%的概率被拒绝；如果达到20个并发连接，则后面的连接将100%被拒绝。\nPasswordAuthentication\nPasswordAuthentication指定是否允许密码登录，默认值为yes（PasswordAuthentication yes），建议改成no（禁止密码登录，只允许密钥登录）。\nPermitEmptyPasswords\nPermitEmptyPasswords指定是否允许空密码登录，即用户的密码是否可以为空，默认为yes（PermitEmptyPasswords yes），建议改成no（禁止无密码登录）。\nPermitRootLogin\nPermitRootLogin指定是否允许根用户登录，默认为yes（PermitRootLogin yes），建议改成no（禁止根用户登录）。\n还有一种写法是写成prohibit-password，表示 root 用户不能用密码登录，但是可以用密钥登录。\n1 PermitRootLogin prohibit-password PermitUserEnvironment\nPermitUserEnvironment指定是否允许 sshd 加载客户端的~/.ssh/environment文件和~/.ssh/authorized_keys文件里面的environment= options环境变量设置。默认值为no（PermitUserEnvironment no）。\nPort\nPort指定 sshd 监听的端口，即客户端连接的端口，默认是22（Port 22）。出于安全考虑，可以改掉这个端口（比如Port 8822）。\n配置文件可以使用多个Port命令，同时监听多个端口。\n1 2 3 4 Port 22 Port 80 Port 443 Port 8080 上面的示例表示同时监听4个端口。\nPrintMotd\nPrintMotd指定用户登录后，是否向其展示系统的 motd（Message of the day）的信息文件/etc/motd。该文件用于通知所有用户一些重要事项，比如系统维护时间、安全问题等等。默认值为yes（PrintMotd yes），由于 Shell 一般会展示这个信息文件，所以这里可以改为no。\nPrintLastLog\nPrintLastLog指定是否打印上一次用户登录时间，默认值为yes（PrintLastLog yes）。\nProtocol\nProtocol指定 sshd 使用的协议。Protocol 1表示使用 SSH 1 协议，建议改成Protocol 2（使用 SSH 2 协议）。Protocol 2,1表示同时支持两个版本的协议。\nPubkeyAuthentication\nPubkeyAuthentication指定是否允许公钥登录，默认值为yes（PubkeyAuthentication yes）。\nQuietMode\nSSH 1 版本专用，指定日志只输出致命的错误信息（QuietMode yes）。\nRSAAuthentication\nRSAAuthentication指定允许 RSA 认证，默认值为yes（RSAAuthentication yes）。\nServerKeyBits\nServerKeyBits指定 SSH 1 版本的密钥重新生成时的位数，默认是768（ServerKeyBits 768）。\nStrictModes\nStrictModes指定 sshd 是否检查用户的一些重要文件和目录的权限。默认为yes（StrictModes yes），即对于用户的 SSH 配置文件、密钥文件和所在目录，SSH 要求拥有者必须是根用户或用户本人，用户组和其他人的写权限必须关闭。\nSyslogFacility\nSyslogFacility指定 Syslog 如何处理 sshd 的日志，默认是 Auth（SyslogFacility AUTH）。\nTCPKeepAlive\nTCPKeepAlive指定打开 sshd 跟客户端 TCP 连接的 keepalive 参数（TCPKeepAlive yes）。\nUseDNS\nUseDNS指定用户 SSH 登录一个域名时，服务器是否使用 DNS，确认该域名对应的 IP 地址包含本机（UseDNS yes）。打开该选项意义不大，而且如果 DNS 更新不及时，还有可能误判，建议关闭。\nUseLogin\nUseLogin指定用户认证内部是否使用/usr/bin/login替代 SSH 工具，默认为no（UseLogin no）。\nUserPrivilegeSeparation\nUserPrivilegeSeparation指定用户认证通过以后，使用另一个子线程处理用户权限相关的操作，这样有利于提高安全性。默认值为yes（UsePrivilegeSeparation yes）。\nVerboseMode\nSSH 2 版本专用，指定日志输出详细的 Debug 信息（VerboseMode yes）。\nX11Forwarding\nX11Forwarding指定是否打开 X window 的转发，默认值为 no（X11Forwarding no）。\n修改配置文件以后，可以使用下面的命令验证，配置文件是否有语法错误。\n1 $ sshd -t 新的配置文件生效，必须重启 sshd。\n1 $ sudo systemctl restart sshd sshd 的命令行配置项 sshd 命令有一些配置项。这些配置项在调用时指定，可以覆盖配置文件的设置：\n（1）-d\n-d参数用于显示 debug 信息。\n1 $ sshd -d （2）-D\n-D参数指定 sshd 不作为后台守护进程运行。\n1 $ sshd -D （3）-e\n-e参数将 sshd 写入系统日志 syslog 的内容导向标准错误（standard error）。\n（4）-f\n-f参数指定配置文件的位置。\n（5）-h\n-h参数用于指定密钥。\n1 $ sshd -h /usr/local/ssh/my_rsa_key （6）-o\n-o参数指定配置文件的一个配置项和对应的值。\n1 $ sshd -o \u0026#34;Port 2034\u0026#34; 配置项和对应值之间，可以使用等号。\n1 $ sshd -o \u0026#34;Port = 2034\u0026#34; 如果省略等号前后的空格，也可以不使用引号。\n1 $ sshd -o Port=2034 -o参数可以多个一起使用，用来指定多个配置关键字。\n（7）-p\n-p参数指定 sshd 的服务端口。\n1 $ sshd -p 2034 上面命令指定 sshd 在2034端口启动。\n-p参数可以指定多个端口。\n1 $ sshd -p 2222 -p 3333 （8）-t\n-t参数检查配置文件的语法是否正确。\nSSH 日志 SSH 在服务器端可以生成日志，记录登录当前服务器的情况。\nSSH 日志是写在系统日志当中的，查看的时候需要从系统日志里面找到跟 SSH 相关的记录。\njournalctl 命令 如果系统使用 Systemd，可以使用journalctl命令查看日志:\n1 $ journalctl -u ssh 1 2 3 4 Mar 25 20:25:36 web0 sshd[14144]: Accepted publickey for ubuntu from 10.103.160.144 port 59200 ssh2: RSA SHA256:l/zFNib1vJ+64nxLB4N9KaVhBEMf8arbWGxHQg01SW8 Mar 25 20:25:36 web0 sshd[14144]: pam_unix(sshd:session): session opened for user ubuntu by (uid=0) Mar 25 20:39:12 web0 sshd[14885]: pam_unix(sshd:session): session closed for user ubuntu ... 上面示例中，返回的日志每一行就是一次登录尝试，按照从早到晚的顺序，其中包含了登录失败的尝试。-u参数是 Unit 单元的意思，-u ssh就是查看 SSH 单元，有的发行版需要写成-u sshd。\n-b0参数可以查看自从上次登录后的日志。\n1 $ journalctl -u ssh -b0 -r参数表示逆序输出，最新的在前面。\n1 $ journalctl -u ssh -b0 -r since和until参数可以指定日志的时间范围。\n1 2 3 4 $ journalctl -u ssh --since yesterday # 查看昨天的日志 $ journalctl -u ssh --since -3d --until -2d # 查看三天前的日志 $ journalctl -u ssh --since -1h # 查看上个小时的日志 $ journalctl -u ssh --until \u0026#34;2022-03-12 07:00:00\u0026#34; # 查看截至到某个时间点的日志 下面的命令查看实时日志。\n1 $ journalctl -fu ssh 其他命令 如果系统没有使用 Systemd，可以在/var/log/auth.log文件中找到 sshd 的日志。\n1 $ sudo grep sshd /var/log/auth.log 下面的命令查看最后 500 行里面的 sshd 条目。\n1 $ sudo tail -n 500 /var/log/auth.log | grep sshd -f参数可以实时跟踪日志。\n1 $ sudo tail -f -n 500 /var/log/auth.log | grep sshd 如果只是想看谁登录了系统，而不是深入查看所有细节，可以使用lastlog命令。\n1 $ lastlog 日志设置 sshd 的配置文件/etc/ssh/sshd_config，可以调整日志级别。\n1 LogLevel VERBOSE 如果为了调试，可以将日志调整为 DEBUG。\n1 LogLevel DEBUG Fail2Ban 教程 简介 Fail2Ban 是一个 Linux 系统的应用软件，用来防止系统入侵，主要是防止暴力破解系统密码。它是用 Python 开发的。\n它主要通过监控日志文件（比如/var/log/auth.log、/var/log/apache/access.log等）来生效。一旦发现恶意攻击的登录请求，它会封锁对方的 IP 地址，使得对方无法再发起请求。\nFail2Ban 可以防止有人反复尝试 SSH 密码登录，但是如果 SSH 采用的是密钥登录，禁止了密码登录，就不需要 Fail2Ban 来保护。\nFail2Ban 的安装命令如下：\n1 2 3 4 5 6 7 8 9 10 # ubuntu \u0026amp; Debian $ sudo apt install fail2ban # Fedora $ sudo dnf install epel-release $ sudo dnf install fail2ban # Centos \u0026amp; Red hat $ yum install epel-release $ yum install fail2ban 安装后，使用下面的命令查看 Fail2Ban 的状态：\n1 $ systemctl status fail2ban 如果没有启动，就启动 Fail2Ban：\n1 $ sudo systemctl start fail2ban 重新启动 Fail2Ban：\n1 $ sudo systemctl restart fail2ban 设置 Fail2Ban 重启后自动运行：\n1 $ sudo systemctl enable fail2ban fail2ban-client Fail2Ban 自带一个客户端 fail2ban-client，用来操作 Fail2Ban：\n1 $ fail2ban-client 上面的命令会输出 fail2ban-client 所有的用法。\n下面的命令查看激活的监控目标(需要额外配置，后面介绍）：\n1 $ fail2ban-client status 输出：\n1 2 3 Status |- Number of jail:\t1 `- Jail list:\tsshd 下面的命令查看某个监控目标（这里是 sshd）的运行情况：\n1 $ sudo fail2ban-client status sshd 输出：\n1 2 3 4 5 6 7 8 9 Status for the jail: sshd |- Filter | |- Currently failed: 1 | |- Total failed: 9 | `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd `- Actions |- Currently banned: 1 |- Total banned: 1 `- Banned IP list: 0.0.0.0 下面的命令输出一个简要的版本，包括所有监控目标被封的 IP 地址：\n1 2 $ sudo fail2ban-client banned [{\u0026#39;sshd\u0026#39;: [\u0026#39;192.168.100.50\u0026#39;]}, {\u0026#39;apache-auth\u0026#39;: []}] 下面的命令可以解封某个 IP 地址：\n1 $ sudo fail2ban-client set sshd unbanip 192.168.1.69 手动禁止一个 IP 地址：\n1 sudo fail2ban-client set sshd banip \u0026lt;IP地址\u0026gt; 配置 主配置文件 Fail2Ban 主配置文件是在/etc/fail2ban/fail2ban.conf，可以新建一份副本/etc/fail2ban/fail2ban.local作为本地配置文件，修改都针对本地配置文件：\n1 $ sudo cp /etc/fail2ban/fail2ban.conf /etc/fail2ban/fail2ban.local 下面是设置 Fail2Ban 的日志位置：\n1 2 [Definition] logtarget = /var/log/fail2ban/fail2ban.log 修改配置以后，需要重新启动fail2ban.service，让其生效。\n封禁配置 Fail2Ban 封禁行为的配置文件是/etc/fail2ban/jail.conf。为了便于修改，可以把它复制一份/etc/fail2ban/jail.local本地配置文件，后面的修改都针对jail.local这个文件：\n1 $ sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local 你也可以在目录/etc/fail2ban/jail.d里面，新建单独的子配置文件，比如/etc/fail2ban/jail.d/sshd.local。\n同样地，修改配置以后，需要重新启动fail2ban.service，让其生效。\n配置文件里面，[DEFAULT]标题行表示对于所有封禁目标生效。举例来说，如果封禁时间修改为1天，/etc/fail2ban/jail.local里面可以写成：\n1 2 [DEFAULT] bantime = 1d 如果某人被封时，对站长发送邮件通知，可以如下设置。\n1 2 3 4 5 6 7 8 9 [DEFAULT] destemail = yourname@example.com sender = yourname@example.com # to ban \u0026amp; send an e-mail with whois report to the destemail. action = %(action_mw)s # same as action_mw but also send relevant log lines #action = %(action_mwl)s 如果配置写在其他标题行下，就表示只对该封禁目标生效，比如写在[sshd]下面，就表示只对 sshd 生效。\n默认情况下，Fail2Ban 对各种服务都是关闭的，如果要针对某一项服务开启，需要在配置文件里面声明。\n1 2 [sshd] enabled = true 上面声明表示，Fail2Ban 对 sshd 开启。\n配置项 下面是配置文件jail.local的配置项含义，所有配置项的格式都是key=value。\n（1）bantime\n封禁的时间长度，单位m表示分钟，d表示天，h表示小时，如果不写单位，则表示秒。Fail2Ban 默认封禁10分钟（10m 或 600）。\n1 2 [DEFAULT] bantime = 10m （2）findtime\n登录失败计算的时间长度，单位m表示分钟，d表示天，如果不写单位，则表示秒。Fail2Ban 默认封禁 10 分钟内登录 5 次失败的客户端。\n1 2 3 4 [DEFAULT] findtime = 10m maxretry = 5 （3）maxretry\n尝试登录的最大失败次数。\n（4）destemail\n接受通知的邮件地址。\n1 2 3 4 [DEFAULT] destemail = root@localhost sender = root@\u0026lt;fq-hostname\u0026gt; mta = sendmail （5）sendername\n通知邮件的“发件人”字段的值。\n（6）mta\n发送邮件的邮件服务，默认是sendmail。\n（7）action\n封禁时采取的动作。\n1 2 [DEFAULT] action = $(action_)s 上面的action_是默认动作，表示拒绝封禁对象的流量，直到封禁期结束。\n下面是 Fail2Ban 提供的一些其他动作。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # ban \u0026amp; send an e-mail with whois report to the destemail. action_mw = %(action_)s %(mta)s-whois[sender=\u0026#34;%(sender)s\u0026#34;, dest=\u0026#34;%(destemail)s\u0026#34;, protocol=\u0026#34;%(protocol)s\u0026#34;, chain=\u0026#34;%(chain)s\u0026#34;] # ban \u0026amp; send an e-mail with whois report and relevant log lines # to the destemail. action_mwl = %(action_)s %(mta)s-whois-lines[sender=\u0026#34;%(sender)s\u0026#34;, dest=\u0026#34;%(destemail)s\u0026#34;, logpath=\u0026#34;%(logpath)s\u0026#34;, chain=\u0026#34;%(chain)s\u0026#34;] # See the IMPORTANT note in action.d/xarf-login-attack for when to use this action # # ban \u0026amp; send a xarf e-mail to abuse contact of IP address and include relevant log lines # to the destemail. action_xarf = %(action_)s xarf-login-attack[service=%(__name__)s, sender=\u0026#34;%(sender)s\u0026#34;, logpath=\u0026#34;%(logpath)s\u0026#34;, port=\u0026#34;%(port)s\u0026#34;] # ban IP on CloudFlare \u0026amp; send an e-mail with whois report and relevant log lines # to the destemail. action_cf_mwl = cloudflare[cfuser=\u0026#34;%(cfemail)s\u0026#34;, cftoken=\u0026#34;%(cfapikey)s\u0026#34;] %(mta)s-whois-lines[sender=\u0026#34;%(sender)s\u0026#34;, dest=\u0026#34;%(destemail)s\u0026#34;, logpath=\u0026#34;%(logpath)s\u0026#34;, chain=\u0026#34;%(chain)s\u0026#34;] （8）ignoreip\nFail2Ban 可以忽视的可信 IP 地址。多个 IP 地址之间使用空格分隔。\n1 ignoreip = 127.0.0.1/8 192.168.1.10 192.168.1.20 （9）port\n指定要监控的端口。可以设为任何端口号或服务名称，比如ssh、22、2200等。\nssh 配置 下面是 sshd 的设置范例：\n1 2 3 4 5 6 7 8 9 10 [sshd] enabled = true port = ssh filter = sshd banaction = iptables backend = systemd maxretry = 5 findtime = 1d bantime = 2w ignoreip = 127.0.0.1/8 首先需要注意，为了让 Fail2Ban 能够完整发挥作用，最好在/etc/ssh/sshd_config里面设置LogLevel VERBOSE，保证日志有足够的信息。\nSSH 端口转发: ? 简介 SSH 除了登录服务器，还有一大用途，就是作为加密通信的中介，充当两台服务器之间的通信加密跳板，使得原本不加密的通信变成加密通信。这个功能称为端口转发（port forwarding），又称 SSH 隧道（tunnel）。\n端口转发有两个主要作用：\n（1）将不加密的数据放在 SSH 安全连接里面传输，使得原本不安全的网络服务增加了安全性，比如通过端口转发访问 Telnet、FTP 等明文服务，数据传输就都会加密。\n（2）作为数据通信的加密跳板，绕过网络防火墙。\n端口转发有三种使用方法：动态转发，本地转发，远程转发。下面逐一介绍。\n动态转发 动态转发指的是，本机与 SSH 服务器之间创建了一个加密连接，然后本机内部针对某个端口的通信，都通过这个加密连接转发。它的一个使用场景就是，访问所有外部网站，都通过 SSH 转发。\n动态转发需要把本地端口绑定到 SSH 服务器。至于 SSH 服务器要去访问哪一个网站，完全是动态的，取决于原始通信，所以叫做动态转发。\n1 $ ssh -D local-port tunnel-host -N 上面命令中，-D表示动态转发，local-port是本地端口，tunnel-host是 SSH 服务器，-N表示这个 SSH 连接只进行端口转发，不登录远程 Shell，不能执行远程命令，只能充当隧道。\n举例来说，如果本地端口是2121，那么动态转发的命令就是下面这样。\n1 $ ssh -D 2121 tunnel-host -N 注意，这种转发采用了 SOCKS5 协议。访问外部网站时，需要把 HTTP 请求转成 SOCKS5 协议，才能把本地端口的请求转发出去。\n下面是 SSH 隧道建立后的一个使用实例。\n1 $ curl -x socks5://localhost:2121 http://www.example.com 上面命令中，curl 的-x参数指定代理服务器，即通过 SOCKS5 协议的本地2121端口，访问http://www.example.com。\n如果经常使用动态转发，可以将设置写入 SSH 客户端的用户个人配置文件（~/.ssh/config）。\n1 DynamicForward tunnel-host:local-port 本地转发 本地转发（local forwarding）指的是，SSH 服务器作为中介的跳板机，建立本地计算机与特定目标网站之间的加密连接。本地转发是在本地计算机的 SSH 客户端建立的转发规则。\n它会指定一个本地端口（local-port），所有发向那个端口的请求，都会转发到 SSH 跳板机（tunnel-host），然后 SSH 跳板机作为中介，将收到的请求发到目标服务器（target-host）的目标端口（target-port）。\n1 $ ssh -L local-port:target-host:target-port tunnel-host 上面命令中，-L参数表示本地转发，local-port是本地端口，target-host是你想要访问的目标服务器，target-port是目标服务器的端口，tunnel-host是 SSH 跳板机。\n举例来说，现在有一台 SSH 跳板机tunnel-host，我们想要通过这台机器，在本地2121端口与目标网站www.example.com的80端口之间建立 SSH 隧道，就可以写成下面这样。\n1 $ ssh -L 2121:www.example.com:80 tunnel-host -N 然后，访问本机的2121端口，就是访问www.example.com的80端口。\n1 $ curl http://localhost:2121 注意，本地端口转发采用 HTTP 协议，不用转成 SOCKS5 协议。\n另一个例子是加密访问邮件获取协议 POP3。\n1 $ ssh -L 1100:mail.example.com:110 mail.example.com 上面命令将本机的1100端口，绑定邮件服务器mail.example.com的110端口（POP3 协议的默认端口）。端口转发建立以后，POP3 邮件客户端只需要访问本机的1100端口，请求就会通过 SSH 跳板机（这里是mail.example.com），自动转发到mail.example.com的110端口。\n上面这种情况有一个前提条件，就是mail.example.com必须运行 SSH 服务器。否则，就必须通过另一台 SSH 服务器中介，执行的命令要改成下面这样。\n1 $ ssh -L 1100:mail.example.com:110 other.example.com 上面命令中，本机的1100端口还是绑定mail.example.com的110端口，但是由于mail.example.com没有运行 SSH 服务器，所以必须通过other.example.com中介。本机的 POP3 请求通过1100端口，先发给other.example.com的22端口（sshd 默认端口），再由后者转给mail.example.com，得到数据以后再原路返回。\n注意，采用上面的中介方式，只有本机到other.example.com的这一段是加密的，other.example.com到mail.example.com的这一段并不加密。\n这个命令最好加上-N参数，表示不在 SSH 跳板机执行远程命令，让 SSH 只充当隧道。另外还有一个-f参数表示 SSH 连接在后台运行。\n如果经常使用本地转发，可以将设置写入 SSH 客户端的用户个人配置文件（~/.ssh/config）。\n1 2 Host test.example.com LocalForward client-IP:client-port server-IP:server-port 远程转发 远程转发指的是在远程 SSH 服务器建立的转发规则。\n它跟本地转发正好反过来。建立本地计算机到远程计算机的 SSH 隧道以后，本地转发是通过本地计算机访问远程计算机，而远程转发则是通过远程计算机访问本地计算机。它的命令格式如下:\n1 $ ssh -R remote-port:target-host:target-port -N remotehost 上面命令中，-R参数表示远程端口转发，remote-port是远程计算机的端口，target-host和target-port是目标服务器及其端口，remotehost是远程计算机。\n远程转发主要针对内网的情况。下面举两个例子:\n第一个例子是内网某台服务器localhost在 80 端口开了一个服务，可以通过远程转发将这个 80 端口，映射到具有公网 IP 地址的my.public.server服务器的 8080 端口，使得访问my.public.server:8080这个地址，就可以访问到那台内网服务器的 80 端口。\n1 $ ssh -R 8080:localhost:80 -N my.public.server 上面命令是在内网localhost服务器上执行，建立从localhost到my.public.server的 SSH 隧道。运行以后，用户访问my.public.server:8080，就会自动映射到localhost:80。\n第二个例子是本地计算机local在外网，SSH 跳板机和目标服务器my.private.server都在内网，必须通过 SSH 跳板机才能访问目标服务器。但是，本地计算机local无法访问内网之中的 SSH 跳板机，而 SSH 跳板机可以访问本机计算机。\n由于本机无法访问内网 SSH 跳板机，就无法从外网发起 SSH 隧道，建立端口转发。必须反过来，从 SSH 跳板机发起隧道，建立端口转发，这时就形成了远程端口转发。跳板机执行下面的命令，绑定本地计算机local的2121端口，去访问my.private.server:80。\n1 $ ssh -R 2121:my.private.server:80 -N local 上面命令是在 SSH 跳板机上执行的，建立跳板机到local的隧道，并且这条隧道的出口映射到my.private.server:80。\n显然，远程转发要求本地计算机local也安装了 SSH 服务器，这样才能接受 SSH 跳板机的远程登录。\n执行上面的命令以后，跳板机到local的隧道已经建立了。然后，就可以从本地计算机访问目标服务器了，即在本机执行下面的命令。\n1 $ curl http://localhost:2121 本机执行上面的命令以后，就会输出服务器my.private.server的 80 端口返回的内容。\n如果经常执行远程端口转发，可以将设置写入 SSH 客户端的用户个人配置文件（~/.ssh/config）。\n1 2 3 Host remote-forward HostName test.example.com RemoteForward remote-port target-host:target-port 完成上面的设置后，执行下面的命令就会建立远程转发。\n1 2 3 4 $ ssh -N remote-forward # 等同于 $ ssh -R remote-port:target-host:target-port -N test.example.com 实例 下面看两个端口转发的实例。\n简易 VPN VPN 用来在外网与内网之间建立一条加密通道。内网的服务器不能从外网直接访问，必须通过一个跳板机，如果本机可以访问跳板机，就可以使用 SSH 本地转发，简单实现一个 VPN。\n1 $ ssh -L 2080:corp-server:80 -L 2443:corp-server:443 tunnel-host -N 上面命令通过 SSH 跳板机，将本机的2080端口绑定内网服务器的80端口，本机的2443端口绑定内网服务器的443端口。\n两级跳板 端口转发可以有多级，比如新建两个 SSH 隧道，第一个隧道转发给第二个隧道，第二个隧道才能访问目标服务器。\n首先，在本机新建第一级隧道。\n1 $ ssh -L 7999:localhost:2999 tunnel1-host 上面命令在本地7999端口与tunnel1-host之间建立一条隧道，隧道的出口是tunnel1-host的localhost:2999，也就是tunnel1-host收到本机的请求以后，转发给自己的2999端口。\n然后，在第一台跳板机（tunnel1-host）执行下面的命令，新建第二级隧道。\n1 $ ssh -L 2999:target-host:7999 tunnel2-host -N 上面命令将第一台跳板机tunnel1-host的2999端口，通过第二台跳板机tunnel2-host，连接到目标服务器target-host的7999端口。\n最终效果就是，访问本机的7999端口，就会转发到target-host的7999端口。\n在远程机器上/etc/ssh/sshd_config 打开 GatewayPorts yes ，才能通过公网访问 远程8080转发 的本地80服务。\n参考链接 An Illustrated Guide to SSH Tunnels, Scott Wiersdorf An Excruciatingly Detailed Guide To SSH, Graham Helton 参考 原文地址： https://wangdoc.com/ssh/ ","date":"2024-08-03T23:59:44+08:00","permalink":"https://arlettebrook.github.io/p/ssh-introduction/","title":"SSH Introduction"},{"content":" 介绍 go-figure可从文本打印出精美的 ASCII 艺术图。它支持FIGlet文件及其大部分功能。\n这个包的灵感来自于 Ruby gem artii，但是是从头开始构建，并且具有不同的功能集。\n它在go语言中通常用于在终端中生成 ASCII 艺术字。该库提供了多种字体和样式，让用户可以轻松地创建视觉上吸引人的文本输出。\n快速使用 首先，使用以下命令安装 go-figure 包：\n1 go get -u github.com/common-nighthawk/go-figure 基本使用：\n1 2 3 4 5 6 7 8 package main import \u0026#34;github.com/common-nighthawk/go-figure\u0026#34; func main() { myFigure := figure.NewFigure(\u0026#34;Hello World\u0026#34;, \u0026#34;\u0026#34;, true) myFigure.Print() } 在上面的代码中，NewFigure 函数创建了一个新的图形对象。第一个参数是要显示的文本，第二个参数是字体名称（默认为标准字体），第三个参数是严格模式开关（true：非ASCII字符报错，反之用?替代）。Print 方法将图形打印到控制台。\n运行上面代码输出：\n1 2 3 4 5 6 $ go run main.go _ _ _ _ _ _ | | | | ___ | | | | ___ __ __ ___ _ __ | | __| | | |_| | / _ \\ | | | | / _ \\ \\ \\ /\\ / / / _ \\ | \u0026#39;__| | | / _` | | _ | | __/ | | | | | (_) | \\ V V / | (_) | | | | | | (_| | |_| |_| \\___| |_| |_| \\___/ \\_/\\_/ \\___/ |_| |_| \\__,_| 创建figure对象 有三种方法可以创建图形对象。它们是： func NewFigure、 func NewColorFigure和 func NewFigureWithFont。\n每个构造函数都接受以下参数：文本、字体和严格模式。“color”构造函数接受颜色作为附加参数。“with font”以不同方式指定字体。具体方法如下：\n1 2 3 func NewFigure(phrase, fontName string, strict bool) figure func NewColorFigure(phrase, fontName string, color string, strict bool) figure func NewFigureWithFont(phrase string, reader io.Reader, strict bool) figure NewFigure只需要字体的名称，并使用存储在 bindata 此包中附带的字体文件。\n如果传递了字体名称的空字符串，则提供默认值（标准字体）。也就是说，这两个都是有效的:\n1 2 3 4 5 myFigure := figure.NewFigure(\u0026#34;Foo Bar\u0026#34;, \u0026#34;\u0026#34;, true) // go-figure/font.go // const defaultFont = \u0026#34;standard\u0026#34; myFigure := figure.NewFigure(\u0026#34;Foo Bar\u0026#34;, \u0026#34;standard\u0026#34;, true) 请注意字体名称区分大小写。\n默认支持的字体如下（用空格分隔）： 3-d 3x5 5lineoblique acrobatic alligator alligator2 alphabet avatar banner banner3-D banner3 banner4 barbwire basic bell big bigchief binary block bubble bulbhead calgphy2 caligraphy catwalk chunky coinstak colossal computer contessa contrast cosmic cosmike cricket cursive cyberlarge cybermedium cybersmall diamond digital doh doom dotmatrix drpepper eftichess eftifont eftipiti eftirobot eftitalic eftiwall eftiwater epic fender fourtops fuzzy goofy gothic graffiti hollywood invita isometric1 isometric2 isometric3 isometric4 italic ivrit jazmine jerusalem katakana kban larry3d lcd lean letters linux lockergnome madrid marquee maxfour mike mini mirror mnemonic morse moscow nancyj-fancy nancyj-underlined nancyj nipples ntgreek o8 ogre pawp peaks pebbles pepper poison puffy pyramid rectangles relief relief2 rev roman rot13 rounded rowancap rozzo runic runyc sblood script serifcap shadow short slant slide slscript small smisome1 smkeyboard smscript smshadow smslant smtengwar speed stampatello standard starwars stellar stop straight tanja tengwar term thick thin threepoint ticks ticksslant tinker-toy tombstone trek tsalagi twopoint univers usaflag wavy weird NewFigureWithFont，直接接受字体文件的reader。这允许您 BYOF（自带字体）。提供 flf 的io.Reader即可。\n字体文件可以在字体文件夹 和figlet.org上找到。 一般不用，默认提供的字体够用了。 NewColorFigure可以制作色彩鲜艳的图形！目前支持的颜色有：蓝色blue，青色cyan，灰色gray，绿色green，紫色purple，红色red，白色white，黄色yellow。\n示例：figure.NewColorFigure(\u0026quot;Hello World\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;cyan\u0026quot;, true).Print() 严格模式规定如何处理标准 ASCII 以外的字符。设置为 true 时，非 ASCII 字符（字符代码 32-127 之外）将导致程序崩溃。设置为 false 时，这些字符将被替换为问号（“？”）。每个示例如下：\nfigure.NewFigure(\u0026quot;Foo 👍 Bar\u0026quot;, \u0026quot;alphabet\u0026quot;, true).Print()\n1 2016/12/01 19:35:38 invalid input. figure.NewFigure(\u0026quot;Foo 👍 Bar\u0026quot;, \u0026quot;alphabet\u0026quot;, false).Print()\n1 2 3 4 5 _____ ___ ____ | ___| ___ ___ |__ \\ | __ ) __ _ _ __ | |_ / _ \\ / _ \\ / / | _ \\ / _` | | \u0026#39;__| | _| | (_) | | (_) | |_| | |_) | | (_| | | | |_| \\___/ \\___/ (_) |____/ \\__,_| |_| 打印figure对象 调用figure对象的Print方法即可将对应艺术字输出的终端。没有返回值。myFigure.Print()\n默认字体推荐 figure.NewFigure(\u0026quot;Arlettebrook\u0026quot;, \u0026quot;standard\u0026quot;, true).Print()\n1 2 3 4 5 _ _ _ _ _ _ / \\ _ __ | | ___ | |_ | |_ ___ | |__ _ __ ___ ___ | | __ / _ \\ | \u0026#39;__| | | / _ \\ | __| | __| / _ \\ | \u0026#39;_ \\ | \u0026#39;__| / _ \\ / _ \\ | |/ / / ___ \\ | | | | | __/ | |_ | |_ | __/ | |_) | | | | (_) | | (_) | | \u0026lt; /_/ \\_\\ |_| |_| \\___| \\__| \\__| \\___| |_.__/ |_| \\___/ \\___/ |_|\\_\\ figure.NewFigure(\u0026quot;Arlettebrook\u0026quot;, \u0026quot;doom\u0026quot;, true).Print()\n1 2 3 4 5 6 ___ _ _ _ _ _ / _ \\ | | | | | | | | | | / /_\\ \\ _ __ | | ___ | |_ | |_ ___ | |__ _ __ ___ ___ | | __ | _ || \u0026#39;__|| | / _ \\| __|| __| / _ \\| \u0026#39;_ \\ | \u0026#39;__| / _ \\ / _ \\ | |/ / | | | || | | || __/| |_ | |_ | __/| |_) || | | (_) || (_) || \u0026lt; \\_| |_/|_| |_| \\___| \\__| \\__| \\___||_.__/ |_| \\___/ \\___/ |_|\\_\\ figure.NewFigure(\u0026quot;Arlettebrook\u0026quot;, \u0026quot;puffy\u0026quot;, true).Print()\n1 2 3 4 5 6 _____ _ _ _ _ _ ( _ ) (_ ) ( )_ ( )_ ( ) ( ) | (_) | _ __ | | __ | ,_)| ,_) __ | |_ _ __ _ _ | |/\u0026#39;) | _ |( \u0026#39;__) | | /\u0026#39;__`\\| | | | /\u0026#39;__`\\| \u0026#39;_`\\ ( \u0026#39;__) /\u0026#39;_`\\ /\u0026#39;_`\\ | , \u0026lt; | | | || | | | ( ___/| |_ | |_ ( ___/| |_) )| | ( (_) )( (_) )| |\\`\\ (_) (_)(_) (___)`\\____)`\\__)`\\__)`\\____)(_,__/\u0026#39;(_) `\\___/\u0026#39;`\\___/\u0026#39;(_) (_) figure.NewFigure(\u0026quot;Arlettebrook\u0026quot;, \u0026quot;big\u0026quot;, true).Print()\n1 2 3 4 5 6 _ _ _ _ _ /\\ | | | | | | | | | | / \\ _ __ | | ___ | |_ | |_ ___ | |__ _ __ ___ ___ | | __ / /\\ \\ | \u0026#39;__| | | / _ \\ | __| | __| / _ \\ | \u0026#39;_ \\ | \u0026#39;__| / _ \\ / _ \\ | |/ / / ____ \\ | | | | | __/ | |_ | |_ | __/ | |_) | | | | (_) | | (_) | | \u0026lt; /_/ \\_\\ |_| |_| \\___| \\__| \\__| \\___| |_.__/ |_| \\___/ \\___/ |_|\\_\\ figure.NewFigure(\u0026quot;Arlettebrook\u0026quot;, \u0026quot;rounded\u0026quot;, true).Print()\n1 2 3 4 5 6 _______ _ _ _ (_______) | | _ _ | | | | _______ ____ | | _____ _| |_ _| |_ _____ | |__ ____ ___ ___ | | _ | ___ | / ___)| | | ___ |(_ _)(_ _)| ___ || _ \\ / ___) / _ \\ / _ \\ | |_/ ) | | | || | | | | ____| | |_ | |_ | ____|| |_) )| | | |_| || |_| || _ ( |_| |_||_| \\_)|_____) \\__) \\__)|_____)|____/ |_| \\___/ \\___/ |_| \\_) 更多样式参考。\n高级用法 动画效果 go-figure 还支持简单的动画效果，如闪烁、滚动和跳舞。\n闪烁 闪烁用到的方法是：func (fig figure) Blink(duration, timeOn, timeOff int)\nduration: 动画持续时间（毫秒）。整个闪烁过程将持续这么长时间。\ntimeOn: 文字亮起的时间（毫秒）。在这段时间内，文字将显示在屏幕上。\ntimeOff: 文字熄灭的时间（毫秒）。在这段时间内，文字将从屏幕上消失。如果 timeOff 设置为 -1，闪烁的间隔将与 timeOn 相同，从而实现均匀的闪烁效果。\n示例：\n1 2 3 colorFigure := figure.NewColorFigure(\u0026#34;Hello World\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;cyan\u0026#34;, true) colorFigure.Blink(3000, 500, -1) colorFigure.Print() 闪烁、滚动和跳舞完成之后，不会打印艺术字，再次调用Print方法打印，效果更好。\n滚动 滚动用到的方法是：func (fig figure) Scroll(duration, stillness int, direction string) duration: 动画持续时间（毫秒）。整个滚动过程将持续这么长时间。\nstillness: 文字在移动前静止的时间（毫秒）。时间越短，滚动速度越快。\ndirection: 滚动的方向，可以是 \u0026ldquo;left\u0026rdquo; 或 \u0026ldquo;right\u0026rdquo;（不区分大小写）。如果提供的方向无效，将默认为 \u0026ldquo;left\u0026rdquo;。\n示例：\n1 2 3 colorFigure := figure.NewColorFigure(\u0026#34;Hello World\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;cyan\u0026#34;, true) colorFigure.Scroll(3000, 300, \u0026#34;left\u0026#34;) colorFigure.Print() 跳舞 跳舞用到的方法是：func (fig figure) Dance(duration, freeze int)\n效果不是特别好。不推荐使用。\nduration: 动画持续时间（毫秒）。整个跳舞过程将持续这么长时间。\nfreeze: 每次跳舞姿势之间的停顿时间（毫秒）。时间越短，动作变化越快。\n示例：\n1 2 3 colorFigure := figure.NewColorFigure(\u0026#34;Hello World\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;cyan\u0026#34;, true) colorFigure.Dance(3000, 300) colorFigure.Print() 输出到其他地方 如果想将输出写入文件或其他 io.Writer，可以使用 Write 方法：\n1 func Write(w io.Writer, fig figure) 该函数接受两个参数：w是一个实现 io.Writer 接口中所有方法的值。 fig是将要写入的图形。没有颜色。\n示例：\n写入文件:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 colorFigure := figure.NewColorFigure(\u0026#34;Hello world\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;cyan\u0026#34;, true) colorFigure.Print() file, err := os.OpenFile(\u0026#34;./demo.txt\u0026#34;, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Panicf(\u0026#34;OpenFile error: %s\u0026#34;, err) } defer func(file *os.File) { if err := file.Close(); err != nil { log.Panicf(\u0026#34;Close file error: %s\u0026#34;, err) } }(file) figure.Write(file, colorFigure) 响应请求：\n1 2 3 4 5 6 7 8 9 10 http.HandleFunc(\u0026#34;/hi\u0026#34;, func(w http.ResponseWriter, r *http.Request) { newFigure := figure.NewFigure(\u0026#34;Arlettebrook\u0026#34;, \u0026#34;\u0026#34;, true) figure.Write(w, newFigure) }) log.Println(\u0026#34;HTTP serve Start running...\u0026#34;) err := http.ListenAndServe(\u0026#34;localhost:8080\u0026#34;, nil) if err != nil { log.Panicf(\u0026#34;Start HTTP serve error: %s\u0026#34;, err) } 启动程序之后，在浏览器中访问 http://localhost:8080/hi 即可输出指定艺术字。\n参考 go-figure官方仓库 ","date":"2024-08-02T15:50:02+08:00","permalink":"https://arlettebrook.github.io/p/go-figure-introduction/","title":"Go-figure Introduction"},{"content":"batch-del-cf-dns-record Batch delete cloudflare DNS records。批量删除cloudflare dns记录。\n介绍 当我们将域名解析到Cloudflare时，不知道什么原因，系统可能会自动导入几百条不那么正确的解析记录，让人抓狂。\n这些记录在界面中无法快速删除，也不支持跳过导入 \u0026hellip;\n手动删除又过于麻烦。\n网上看了一下。我们可以通过Cloudflare 的API功能来实现批量删除解析。因此写了一个Go版本的脚本。仅供参考。\n地址是: https://github.com/arlettebrook/batch-del-cf-dns-record\n如何使用 帮助信息：\n1 2 3 4 5 6 $ ./batch-del-cf-dns-record.exe --help Usage of Batch-del-cf-dns-record: -l, --log_level string Log level (default \u0026#34;info\u0026#34;) -a, --api_token string Cloudflare API Token -z, --zone_id string Cloudflare Zone ID pflag: help requested 前提条件 登录cloudflare账号\n进入要删除DNS记录域名的首页\n滑动到底部右下角，有一个我们需要的第一个参数：区域ID(zone_id)\n最后一个参数：api_token\n点击下面的获取您的API令牌，创建令牌 创建一个编辑区域的DNS令牌。 这个令牌就是api_token 使用完成之后可以删除 源码运行 前提条件：需要Go环境\n克隆项目到本地\n1 git clone https://github.com/arlettebrook/batch-del-cf-dns-record.git 进入项目，安装依赖，运行main.go并指定参数即可。\n1 2 3 4 5 cd batch-del-cf-dns-record go mod tidy go run main.go -a api_token -z zone_id 二进制文件运行[推荐] 注意：只提供了Windows版本的二进制文件，其他系统自行编译。\n前往发布页面下载，最新Windows版本。\n在Windows终端中运行batch-del-cf-dns-record.exe并指定参数即可:\n1 batch-del-cf-dns-record.exe -a api_token -z zone_id 如果是bash之类的终端，运行：\n1 ./batch-del-cf-dns-record.exe -a api_token -z zone_id 注意事项 如果是TLS握手超时，重新运行即可。 ","date":"2024-07-22T23:55:50+08:00","permalink":"https://arlettebrook.github.io/p/batch-del-cf-dns-record/","title":"Batch Del CF DNS Record"},{"content":" 安装vim Vim （官网） 是一个非常流行的文本编辑器，可以在多种操作系统上安装和使用。下面是如何在不同系统上安装 Vim 的方法：\n在 Linux 上安装 Vim 大多数 Linux 发行版都在其软件包管理器中包含 Vim。可以使用包管理器来安装它。\nDebian/Ubuntu 系列\n1 2 sudo apt update sudo apt install vim CentOS\n1 sudo yum install vim 在 macOS 上安装 Vim macOS 通常预装了 Vim，但可能不是最新版本。你可以使用 Homebrew 来安装或更新 Vim。\n使用 Homebrew 安装\n确保 Homebrew 已安装。你可以在终端中运行以下命令来安装 Homebrew：\n1 /bin/bash -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\u0026#34; 使用 Homebrew 安装 Vim：\n1 2 brew update brew install vim 在 Windows 上安装 Vim 在 Windows 上，你可以通过以下方法安装 Vim：\n直接下载安装程序:\n访问 Vim 官方下载页面。推荐vim-win32-installer（会及时提供最新编译版）。\n在 \u0026ldquo;PC: MS-DOS and MS-Windows\u0026rdquo; 部分，下载适用于 Windows 的安装程序（通常是 gvim 安装程序）。\n运行下载的安装程序，按照提示完成安装。\n安装完成之后建议添加如下两个环境变量。\nPath环境变量：添加的值为vim安装路径里面的具体版本。如：\nD:\\Vim\\vim91 or %VIM%\\vim91\n环境变量VIM：值为vim的安装路径。如\nWindows版本的vim提供了卸载程序。在程序和功能中找到即可卸载。\nWindows安装结束后，你会发现开始菜单中有好多Vim，而且名字都不一样。\n他们的区别如下：\n有g和没有g gVim 是在windows下的Gui图形用户界面的的 vim (GUI Vim)，支持windows的一些快捷方式，支持鼠标操作 vim 是在windows下的类似linux vi 编辑的界面，只能用键盘操作。 vim的操作指令同时适用于gVim Vim, Vim Diff, Vim Easy, Vim Read-only Diff 是用来对比两个文件内容用的，直接打开挺没用的，不过直接拖2个文件到快捷方式上倒是可行； Easy启动的时候是insert模式，适合普通windows用户的习惯； Read-Only的用途：比如用read-only打开已经用vim打开过的文件，就不会提示让人烦躁的.swp文件存在的问题； 通用方法：编译安装 Vim 如果你希望安装最新版本的 Vim 或自定义编译选项，可以从源代码编译安装。\n安装必要的依赖项（以 Ubuntu 为例）：\n1 2 sudo apt update sudo apt install git make ncurses-dev gcc 克隆 Vim 的源代码仓库：\n1 2 git clone https://github.com/vim/vim.git cd vim 配置并编译 Vim：\n1 2 ./configure make 安装 Vim：\n1 sudo make install 在IDE中安装vim插件 许多现代集成开发环境（IDE）提供了 Vim 模拟插件。我们在享受 IDE 强大功能的同时，可以继续使用Vim的编辑风格。\n要在IDE中安装vim插件，只需要在对应IDE的插件市场搜索安装即可，我常用的：\nJetBrains系列：IdeaVim 可以配置 ~/.ideavimrc 文件来个性化设置。 Visual Studio Code (VS Code)：Vim 可通过 settings.json 文件进行配置和自定义。 Sublime Text：Vintageous 可以通过 Sublime Text 的设置进行配置和自定义。 …… 配置vim Vim 是一个高度可配置的文本编辑器，可以通过编辑其配置文件来调整和扩展其功能。\n配置文件介绍 Vim没有提供图形化的配置界面，配置Vim都是通过配置文件（.vimrc）实现的：\n在 Unix 或 Linux 系统中，.vimrc 文件通常位于用户的主目录中，例如 ~/.vimrc。\n在 Windows 系统中，.vimrc 文件可以放在用户主目录下，例如 C:\\Users\\\u0026lt;username\u0026gt;\\_vimrc，或者 C:\\Users\\\u0026lt;username\u0026gt;\\.vimrc。\n**总结：Vim配置文件都在用户的家目录下，名称都可以用.vimrc表示。**没有手动创建一个空文件，直接使用。\n要修改配置，就在.vimrc文件中修改就行。\n注意事项：\n注释是以\u0026quot;开头。\n部分vim插件版的配置文件名并不是.vimrc：\n如ideavim：~/.ideavimr。 这里总结一下git bash内置的vim插件：\n该vim版本也可以用~/.vimrc进行配置值。\n默认的配置文件在git安装位置/etc/vimrc。只读。 如果期望git bash不使用内置的vim插件，使用的是自己安装的vim版本：\n可以修改git bash的配置文件.bashrc，添加vim的安装位置。 1 export PATH=\u0026#34;/d/vim/vim91:$PATH\u0026#34; 原理：使自己安装的vim版本优先级最高。先加载。尽管我在PATH变量中添加了自己安装的vim版本路径，但是无法保证谁的优先级高，所以使用了上面办法，使自己安装的vim版本优先级最高。先加载。 常见的 Vim 配置项 .vimrc：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 \u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;基本设置\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34; filetype on \u0026#34;开启文件类型侦测 filetype indent on \u0026#34;适应不同语言的缩进 syntax enable \u0026#34;开启语法高亮功能 syntax on \u0026#34;允许使用用户配色 \u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;显示设置\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34; set shortmess=atI \u0026#34;不显示启动提示信息 set laststatus=2 \u0026#34;总是显示状态栏,命令行（在状态行下）的高度，默认为1，这里是2。 \u0026#34; 我的状态行显示的内容（包括文件类型和解码）后续用插件美化。 \u0026#34;set statusline=%F%m%r%h%w\\ [FORMAT=%{\u0026amp;ff}]\\ [TYPE=%Y]\\ [POS=%l,%v][%p%%]\\ %{strftime(\\\u0026#34;%d/%m/%y\\ -\\ %H:%M\\\u0026#34;)} \u0026#34;set statusline=[%F]%y%r%m%*%=[Line:%l/%L,Column:%c][%p%%] \u0026#34;set cmdheight=2 \u0026#34; 命令行（在状态行下）的高度，默认为1，这里是2 set ruler \u0026#34;显示光标位置 set number \u0026#34;显示行号 \u0026#34;set cursorline \u0026#34;高亮显示当前行 \u0026#34;set cursorcolumn \u0026#34;高亮显示当前列 set hlsearch \u0026#34; 高亮搜索结果 set incsearch \u0026#34;边输边高亮 set ignorecase \u0026#34;搜索时忽略大小写 set smartcase\t\u0026#34; 智能大小写匹配 \u0026#34;set relativenumber \u0026#34;其他行显示相对行号 set scrolloff=5 \u0026#34;垂直滚动时光标距底部位置 \u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;编码设置\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34; set fileencodings=utf-8,gb2312,gbk,gb18030,cp936 \u0026#34; 检测文件编码,将fileencoding设置为最终编码 set fileencoding=utf-8 \u0026#34; 保存时的文件编码 set termencoding=utf-8 \u0026#34; 终端的输出字符编码 set encoding=utf-8 \u0026#34; VIM打开文件使用的内部编码 \u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;编辑设置\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34; set expandtab \u0026#34;扩展制表符为空格 set tabstop=4 \u0026#34;制表符占空格数 set softtabstop=4 \u0026#34;将连续数量的空格视为一个制表符 set shiftwidth=4 \u0026#34;自动缩进所使用的空格数 set textwidth=80 \u0026#34;设置一行内容的宽度 set linebreak \u0026#34;防止单词内部折行 set wrapmargin=5 \u0026#34;指定折行处与右边缘空格数 set smarttab \u0026#34;使用智能制表符 set smartindent \u0026#34;智能缩进(好处是修改代码时会根据代码规则自动缩进，坏处是当用`:n,m\u0026gt;`对齐左侧的注释将不会被移动) \u0026#34;set autoindent \u0026#34;自动缩进(这两个差不多，感觉在大括号自动配对时，用智能缩进好点) set wildmenu \u0026#34;vim命令自动补全 set autochdir \u0026#34;自动定位当前目录。 set wrap \u0026#34;启用自动换行\u0026#34; set autoread \u0026#34;文件改动时自动载入 set t_Co=256 \u0026#34;terminal Color 支持256色(默认是8色) hi comment ctermfg=6 \u0026#34;设置注释颜色 set magic \u0026#34; 设置魔术 set guioptions-=T \u0026#34; 隐藏gui工具栏 set guioptions-=m \u0026#34; 隐藏gui菜单栏 set guioptions-=r \u0026#34; 删去gui滚动条\u0026#34; \u0026#34; 使用更友好的颜色方案 colorscheme desert \u0026#34; 设置背景色 set background=dark \u0026#34; 显示命令输入 set showcmd \u0026#34;设置gui字体 set guifont=Courier\\ New:h20 \u0026#34; 设置宽高 \u0026#34;set lines=15 columns=50 \u0026#34; 启用真彩色颜色支持，让配色方案显示更好。 set termguicolors 注意配置了上面大部分设置，在大多数主题中只需要在vim-plug后面选择颜色方案colorscheme即可。\n插件使用 Vim 的功能可以通过插件进一步扩展。使用插件可以增强Vim的功能，如语法高亮、代码补全、文件浏览器、版本控制集成、模糊搜索等。\n为了方便安装，更新，删除插件，我们一般使用插件管理器进行插件管理。\n推荐使用的插件管理器是vim-plug。与其他插件管理器（如 Vundle 和 Pathogen）相比，vim-plug 提供了更快的性能和更多的功能，如并行安装插件、延迟加载插件、使用简单等。\n安装 vim-plug 下载并安装 vim-plug： 在终端中运行以下命令，这会将 vim-plug 下载到你的 Vim 自动加载目录下：\nLinux：\n1 2 curl -fLo ~/.vim/autoload/plug.vim --create-dirs \\ https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim Windows：\n1 2 curl -fLo ~/vimfiles/autoload/plug.vim --create-dirs \\ https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim 注意：需要拥有curl命令，并且终端能够访问外网。\n如果没有，可以手动下载plug.vim，并将文件放在 windows 中的 ~/vimfiles/autoload 或 Linux 中的 ~/.vim/autoload 文件夹内，没有手动创建。\n插件推荐 状态栏和主题\nlightline.vim：轻量级状态栏插件。 vim-airline：高级状态栏插件，提供丰富的功能和美观的主题。 **dracula-theme**主题 **material.vim**主题 **vim-transparent**终端透明。 这个插件，可以配置让其他插件也透明。 vim-deus主题 配置主题建议都配置set termguicolors\u0026quot; 启用真彩色颜色支持，让配色方案显示更好。会让终端透明效果更好。 如果喜欢用gvim(我用sublme的Vim插件)。可以：\n全屏显示：\ngvimfullscreen_win32 32位系统: - 下载gvimfullscreen.dll - 将其放入gvim.exe同目录的文件夹下 - 配置文件中map \u0026lt;F11\u0026gt; \u0026lt;Esc\u0026gt;:call libcallnr(\u0026quot;gvimfullscreen.dll\u0026quot;, \u0026quot;ToggleFullScreen\u0026quot;, 0)\u0026lt;CR\u0026gt;\n64位系统同上, 用gvimfullscreen_64代替\n现在你就可以用\u0026lt;F11\u0026gt;来进行全屏操作\n透明显示\nvimtweak\n方法基本同上, 选择vimtweak32.dll或vimtweak64.dll放入文件夹, 在vimrc中\n1 au GUIEnter * call libcallnr(\u0026#34;vimtweak64.dll\u0026#34;, \u0026#34;SetAlpha\u0026#34;, 200) 其中数值200可以选择0-255, 255为不透明\n一组默认配置（每个人都同意默认的配置）：vim-sensible：插件的功能：\n'backspace'：在插入模式下按退格键可删除任何内容。默认只能删除新添加的。 'incsearch'：按回车键之前开始搜索。 'listchars'：使:set list（可见空白）更漂亮。 'scrolloff'：始终在光标上方/下方显示至少一行。 'autoread'：自动加载文件更改。您可以按 撤消u。 runtime! macros/matchit.vim：加载 Vim 附带的 matchit.vim 版本。 文件浏览和导航\nNERDTree：文件系统浏览器，提供树状目录视图。 也可以用内置的:Vex浏览目录。它没有目录树结构。 fzf.vim：模糊查找工具，基于 fzf 命令行工具。 语法高亮和语法检查\nvim-polyglot：支持多种编程语言的语法高亮。 ALE：异步语法检查和修复工具。 代码补全\nYouCompleteMe：强大的代码补全插件，支持多种编程语言。 coc.nvim：基于 VSCode 插件的代码补全和语言服务器支持。 没打算用vim写代码，没花时间去配置。 版本控制\nvim-fugitive：Git 集成插件，提供强大的 Git 操作支持。 gitgutter：在编辑器中显示 Git 的改动信息。 都是在终端中使用git，我没有配置。 其他实用插件\nvim-highlightedyank：让复制区域高亮。（ideavim内置插件。） auto-pairs：成对添加、删除、高亮括号。 surround.vim：轻松操作成对符号（例如引号、括号）。 auto-pairs的扩展。成对修改括号为别的括号。如cs\u0026quot;'将成对\u0026quot;改为'。 commentary.vim：快速注释和取消注释代码。 在普通模式下，移动光标到要注释的行，然后使用 gcc 注释/取消注释当前行。 在可视模式下，选择要注释的代码块，然后使用 gc 注释/取消注释选定的代码。 很智能，好用。（ideavim内置插件。） 配置插件 编辑 .vimrc 文件，添加插件管理器配置：\nwindwos为例，将插件安装位置保存在$VIM/vimfiles/plugged，将下面代码追加到.vimrc中：\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 \u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;插件vim-plug\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34; \u0026#34; 初始化 vim-plug \u0026#34; Linux上默认 \u0026#34; call plug#begin() \u0026#34; windows上自定义插件安装位置 call plug#begin(\u0026#39;$VIM/vimfiles/plugged\u0026#39;) \u0026#34; The default plugin directory will be as follows: \u0026#34; - Vim (Linux/macOS): \u0026#39;~/.vim/plugged\u0026#39; \u0026#34; - Vim (Windows): \u0026#39;~/vimfiles/plugged\u0026#39; \u0026#34; - Neovim (Linux/macOS/Windows): stdpath(\u0026#39;data\u0026#39;) . \u0026#39;/plugged\u0026#39; \u0026#34; You can specify a custom plugin directory by passing it as the argument \u0026#34; - e.g. `call plug#begin(\u0026#39;~/.vim/plugged\u0026#39;)` \u0026#34; - Avoid using standard Vim directory names like \u0026#39;plugin\u0026#39; \u0026#34; 添加插件列表,确保使用的是单引号。 \u0026#34; Plug \u0026#39;tpope/vim-sensible\u0026#39; \u0026#34; Plug \u0026#39;scrooloose/nerdtree\u0026#39; \u0026#34; Plug \u0026#39;itchyny/lightline.vim\u0026#39; \u0026#34; Call plug#end to update \u0026amp;runtimepath and initialize the plugin system. \u0026#34; - It automatically executes `filetype plugin indent on` and `syntax enable` \u0026#34; 结束插件配置 call plug#end() \u0026#34; You can revert the settings after the call like so: \u0026#34; filetype indent off \u0026#34; Disable file-type-specific indentation \u0026#34; syntax off \u0026#34; Disable syntax highlighting \u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;插件vim-plug结束\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34; 添加插件：我们只需要将插件添加到Plug ''里面即可，别忘记取消注释。插件名一般为为github作者/仓库名。\n安装插件：打开vim运行:PlugInstall命令来安装 .vimrc 中定义的插件。\n更新插件： 要更新所有插件，运行：:PlugUpdate\n清除未使用的插件： 如果你从配置中移除了一些插件，可以运行:PlugClean命令来删除未使用的插件。\n检查插件状态： 要检查插件的状态和版本，运行:PlugStatus。\nDone表示成功，:q退出插件管理器。失败退出重试。或R重试。\n插件安装成功示例：\n效果展示 vim配置效果：\nmaterial：default：\nmaterial：palenight：\n区别不是很大。\ndeus：\n我的完整配置。\n扩展 自定义快捷键 在Vim中自定义快捷键，其实就创建快捷键与命令之间的映射关系。\n用到的命令是map或noremap。注意：要将命令配置在.vimrc中。\n格式：映射命令 自定义快捷键 命令，中间用空格分隔开。\n前面有n, i, v, c中的任意一个字符表示对应的模式。如nmap或nnoremap表示Normal模式下的映射关系。没有表示所有模式的映射关系。\n二者区别：\nmap会递归解析映射。\nnoremap不会递归解析映射。\n意思是如果命令中有别的映射，一个会解析，一个不会。如：\n1 2 3 nmap yy dd # 修改了默认映射。 nnoremap ,y \u0026#34;*yy # 会执行复制一行命令到剪切板。 nmap ,d \u0026#34;*yy # 会删除一行命令到剪切板。解析了yy映射。 看需求使用，是否需要递归映射。没有需求推荐使用noremap创建映射，防止映射多了，递归解析，导致无限循环或意外行为。\n注意事项：\n当自定义的快捷键与vim预定义的快捷键冲突时，自定义的快捷键会覆盖预定义的快捷键。\n查看自定义的快捷键:map。或者查看.vimrc。\n使用 :verbose map 命令可以查看详细的映射信息，包括映射是在哪里定义的。 取消自定义的快捷键：在自定义快捷键的后面添加u+自定义的快捷键。如\n1 2 3 nnoremap ,y \u0026#34;*yy # 自定义快捷键 ... unnoremap ,y # 取消快捷键 常用键表示法\n普通字符：\n直接输入字母、数字、符号。例如：a, b, 1, 2, #, *。 控制键：\n使用 \u0026lt;Ctrl\u0026gt;（或 \u0026lt;C\u0026gt;）表示控制键。例如：\u0026lt;C-a\u0026gt; 表示 Ctrl+a。 功能键：\n使用 \u0026lt;F1\u0026gt; 到 \u0026lt;F12\u0026gt; 表示功能键。例如：\u0026lt;F2\u0026gt; 表示功能键 F2。 特殊键：\n使用尖括号包围的特殊键表示法。例如： \u0026lt;Esc\u0026gt;：Escape 键 \u0026lt;CR\u0026gt;：回车键（Enter） \u0026lt;Tab\u0026gt;：制表符键（Tab） \u0026lt;Space\u0026gt;：空格键（Space） \u0026lt;BS\u0026gt;：退格键（Backspace） \u0026lt;Del\u0026gt;：删除键（Delete） \u0026lt;Up\u0026gt;：上箭头键 \u0026lt;Down\u0026gt;：下箭头键 \u0026lt;Left\u0026gt;：左箭头键 \u0026lt;Right\u0026gt;：右箭头键 组合键：\n可以组合使用。例如：\u0026lt;C-Space\u0026gt; 表示 Ctrl+Space，\u0026lt;C-Left\u0026gt; 表示 Ctrl+Left。 Leader 键\n使用 \u0026lt;leader\u0026gt; 键可以避免与默认快捷键冲突。默认情况下，\u0026lt;leader\u0026gt; 键是反斜杠 \\，但你可以在 .vimrc 中重新定义它：\n1 let mapleader = \u0026#34;,\u0026#34; 然后你可以使用 \u0026lt;leader\u0026gt; 键创建自定义快捷键：\n1 nnoremap \u0026lt;leader\u0026gt;y \u0026#34;*yy 这样，你的自定义快捷键将使用 ,y 来触发，并且\u0026lt;leader\u0026gt;可以更改为你喜欢但不冲突的键。逗号跟反斜杠就是，不过我更喜欢用逗号。\n示例：在vim中没有快捷键删除光标后面的字符。我们来自定义映射实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026#34; ctrl-k：删除光标后面的字符，部分版本不支持 nnoremap \u0026lt;C-k\u0026gt; D inoremap \u0026lt;C-k\u0026gt; \u0026lt;Esc\u0026gt;lC vnoremap \u0026lt;C-k\u0026gt; d \u0026#34; 定义删除光标后所有字符的函数 function! DeleteAfterCursor() \u0026#34; 获取当前光标在命令行中的位置 let current_pos = getcmdpos() \u0026#34; 获取当前命令行的内容 let cmdline = getcmdline() \u0026#34; 截取字符串，获取光标前的部分 let new_cmdline = strpart(cmdline, 0, current_pos - 1) \u0026#34; 设置新的命令行内容，只保留光标前的部分，去掉光标位置及其后的所有字符 call setcmdline(new_cmdline) return \u0026#39;\u0026#39; endfunction \u0026#34; 在命令行模式下映射 \u0026lt;C-k\u0026gt; 为删除光标后的所有字符 \u0026#34; 用表达式寄存器调用删除函数，最后回车 cnoremap \u0026lt;C-k\u0026gt; \u0026lt;C-r\u0026gt;=DeleteAfterCursor()\u0026lt;CR\u0026gt; 通过上面的命令，我们实现了终端在Emacs模式下的ctrl-k快捷键。但是只能在vim中使用，并且vim版本需要支持表达式寄存器。\n如果终端的编辑模式为vi，依旧不能解决问题。在insert模式下不能使用ctl-k快捷键。需要修改终端的映射关系.inputrc。不过够用了。\n示例：配置复制、粘贴、剪切快捷键：\n1 2 3 4 5 6 \u0026#34; 配置复制、粘贴、剪切 vnoremap \u0026lt;C-c\u0026gt; \u0026#34;*y inoremap \u0026lt;C-v\u0026gt; \u0026lt;C-r\u0026gt;* cnoremap \u0026lt;C-v\u0026gt; \u0026lt;C-r\u0026gt;* vnoremap \u0026lt;C-v\u0026gt; \u0026#34;*p vnoremap \u0026lt;C-x\u0026gt; \u0026#34;*d 需要注意的是不要配置普通模式下的ctrl-v，它是可视块的快捷键，可以配置visual模式的，需要按两次才能粘贴。不过够用了。\n命令参考：Vim Common Commands\n","date":"2024-05-30T21:52:12+08:00","permalink":"https://arlettebrook.github.io/p/vim-introduction/","title":"Vim Introduction"},{"content":" 概念 终端：命令行交互界面。如： windows terminal、windterm、FinalShell。PowerShell。 控制台：一种特殊的终端。范围更广，既可以指硬件也可以指软件工具，常用于系统管理和监控。 TTY：终端的文本输入输出接口。可以理解为就是终端。不深入了解😂。 Shell：命令行解释器。如： Bash、Zsh、PowerShell、git bash。 终端通过TTY与Shell通信。 推荐文章：命令行界面 (CLI)、终端 (Terminal)、Shell、TTY，傻傻分不清楚？\nzsh安装 Bash是Linux系统内置的shell，提供了强大的命令行编辑、脚本编写和命令历史功能，广泛用于 Linux 和 macOS 系统中。是目前最流行的 Shell 之一。\n而Zsh 是一个功能强大的 Shell，具有比 Bash 更多的特性，如更高级的自动补全、更强大的脚本能力和更丰富的配置选项，还提供了诸如共享历史、拼写校正、主题支持和插件系统等增强功能，使其成为高级用户和开发者的首选。\n接下来介绍如何安装：\nwindows上安装 注意：windows上安装zsh是建立在git bash基础上的。\n所以只有安装了git，我们才能在windows上使用zsh。\n安装git：git官网。\n个人体验：利用上面方法在windows上使用zsh，效果不是特别好，不知道是不是我电脑配置低的原因：每次利用git bash启动zsh都很慢。所以windows上我用的shell一直都是git bash。\n下载zsh对应的windows版本，官方并没有提供，由MSYS2提供。（官方下载需要用包管理工具）\nMSYS2 是针对Windows 的软件分发和构建平台。 windows上如果没有包管理工具，只能去这里下载：\n地址、备用\n后续教程是以文件下载为例。\n下载完成之后将压缩包解压，用rar就可以解压。\n将解压的内容全部剪切到git的安装目录。\n需要权限的话就授权（可能需要多次授权），重名的话直接覆盖。\n打开 Git Bash 标签页或者直接右键打开 Git bash 输入 zsh，出现下图则安装成功：\n暂时先不进行其他设置，直接输入 0 结束并生成 .zshrc 配置文件即可。\n该文件在当前用户的家目录，win+r输入.回车进入就是。\n由于现在没有安装 zsh 主题，可以这样区分 bash 和 zsh，bash的光标在第二行，zsh的光标在同一行：\n设置默认启动\n每次打开 Git Bash 终端，你会发现默认还是 Bash ，而不是 Zsh，可以通过编辑 Bash 终端的配置文件 .bashrc 来实现默认使用 Zsh，在 Git Bash 终端中输入命令：\n1 vim ~/.bashrc Vim 默认是命令模式，你可以直接用文本编辑器打开将配置内容粘贴进去：\n1 2 3 if [ -t 1 ]; then #1表示标准输出，用于判断标准输出是否连接到终端（tty），如果是，则执行 zsh 命令来切换为 Zsh Shell。 exec zsh fi 后面是vim的常用命令，a、shift+insert、esc、+:wq最后按回车键，保存退出\n注意：注释（#）调这三行代码，启动git bash时就不会启用zsh。\n之后再打开 Git Bash 终端，默认就会使用 Zsh 了。第一次可能有一个警告：大概是找不到 ~/bash_profile 等一些文件，可以忽略，以后不会再出现了。\n至此windows上安装zsh完成。\nLinux上安装 准备：\n查看当前 shell\n1 2 echo $SHELL echo $0 # or 安装 zsh\nCentOS：要管理员身份\n1 yum install -y zsh Ubuntu：\n1 sudo apt install -y zsh 将zsh替换为默认shell\n为 root 设置默认 shell 1 chsh -s /bin/zsh 返回结果如下，表示切换完成（下载安装 oh-my-zsh 成功后也会提示切换）\n为特定用户设置默认 shell\n1 2 sudo chsh -s /bin/zsh \u0026lt;username\u0026gt; # \u0026lt;username\u0026gt; 替换为实际用户名 在 CentOS 8 中可能报错 Command not found，执行 sudo dnf install util-linux-user 重新登录shell之后，默认就是zsh\n至此Linux安装zsh完成。\n安装 Oh My Zsh 成功安装了zsh，还需要安装Oh My Zsh，它对zsh进行了扩展，这也是为什么要使用zsh了，而不是bash的原因。\n注意：以下方法同适用于安装了zsh的环境，如Linux。\n在安装好 Zsh 终端之后，看起来跟 Bash 终端并无太大的区别，我们也没有进行设置。而 Oh My Zsh 可以用于管理 Zsh配置。它捆绑了数千个有用的功能、助手、插件、主题等。\n官方： https://github.com/ohmyzsh/ohmyzsh\n在命令行输入命令并按回车执行：\n1 sh -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\u0026#34; 这条命令国内需要开代理，且模式为Tun终端才能访问。官方有提供国内的下载地址，失败多试几次。\n1 sh -c \u0026#34;$(curl -fsSL https://install.ohmyz.sh/)\u0026#34; 还需要有curl以及git工具，我MinGW默认安装了curl命令，如果失败可以下载ohmyzsh安装脚本本地运行。\n1 sh ./install.sh #离线安装,注意要进入脚本的下载位置 出现下图的内容就是安装成功了，如果出现错误，或长时间没有响应，多试几次即可：\n最后一行的 ERROR 可以忽略，windows正常，Linux没有\n​\t配置 zsh Zsh的配置文件在用户的家目录，文件名是 .zshrc，编辑配置文件，可以对 Zsh进行一些定制化配置：\n1 vim ~/.zshrc 编辑并保存配置文件之后，并不会立即生效，可以关闭所有终端重新打开，或者使用命令让配置生效：\nsource可以替换成.\n1 2 source ~/.zshrc . ~/.zshrc # or 配置主题 就是对.zshrc配置\nOh My Zsh 安装默之后，默认使用主题是 robbyrussell，可以修改 .zshrc 配置中的 ZSH_THEME 字段，所有可用主题可参考ohmyzsh官方主题页面，这里先配置一下我个人比较喜欢的主题：gentoo or eastwood or daveverwer or bira\n注意：ZSH_THEME只能修改为官方提供的主题名，然后重新加载.zshrc文件，就能修改为指定的主题。\n官方提供的主题都保存在~/.oh-my-zsh/themes目录，你也可以自定义主题。 配置插件 插件Oh My Zsh 附带了大量插件供您使用。您可以查看插件目录和/或wiki，了解当前可用的内容。\n通过使用插件，可以让 Zsh 的功能更加强大，Zsh 和 Oh My Zsh 自带了一些实用的插件，也可以下载其他的插件。 如 Zsh 自带 Git 插件，可以在命令行显示 Git 相关的信息，并提供了一些操作 Git 的别名：\n1 2 3 4 5 gaa = git add --all gcmsg = git commit -m ga = git add gst = git status gp = git push 自动补全 zsh-autosuggestions 插件，可以在你历史指令中找到与你当前输入指令匹配的记录，并高亮显示，如果想直接使用，可以直接通过右方向键补全。 安装插件，在终端分别执行下面两条命令：官方zsh-autosuggestions\n1 2 3 cd ~/.oh-my-zsh/custom/plugins #指定了克隆的位置，就是什么切换的地方 git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions 插件下载完成之后，编辑 ~/.zshrc 配置文件，修改插件相关配置项：\n1 vim ~/.zshrc 插件下载完成之后，编辑 ~/.zshrc 配置文件，修改插件相关配置项：\n1 vim ~/.zshrc 请注意，插件由空格（空格、制表符、换行符\u0026hellip;）分隔。请勿在它们之间使用逗号，否则会损坏。\n保存退出之后，记得使用命令 source ~/.zshrc 重载配置。该插件生效之后，在使用命令的时候，就会匹配我们使用的命令，右键可以直接补全：\n如果你不喜欢提示默认的浅灰色，可以在 ~/.zshrc 中修改（没有配置项就添加），更多配置可以参考zsh-autosuggestions官方文档：\n1 ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE=\u0026#34;fg=#9fc5e8\u0026#34; 目录跳转 Zsh 自带有一个插件 z，可以让我们在访问过的目录中快速跳转，将该插件配置到 ~/.zshrc 文件中即可使用：\n保存退出之后，重载配置，随意进入一些目录，之后再使用命令 z 就可以实现快速跳转，支持模糊匹配：\n或许相比于 z，更多人会选择使用 autojump，如果是 Mac 或者 Linux 没什么问题，Windows 就不太建议折腾了。\n其他插件 zsh-syntax-highlighting：这个插件可以识别的 shell 命令并高亮显示\n1 git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-${ZSH:-~/.oh-my-zsh}/custom}/plugins/zsh-syntax-highlighting zsh-completions：额外的自动补全功能，用于补充 zsh 中尚不支持的命令补全，该项目将在完善时合并到 zsh。\n1 git clone https://github.com/zsh-users/zsh-completions ${ZSH_CUSTOM:-${ZSH:-~/.oh-my-zsh}/custom}/plugins/zsh-completions 然后在.zshrc文件里面的source \u0026quot;$ZSH/oh-my-zsh.sh\u0026quot;这一行前添加以下代码\n1 fpath+=${ZSH_CUSTOM:-${ZSH:-~/.oh-my-zsh}/custom}/plugins/zsh-completions/src Note: adding it as a regular Oh My ZSH! plugin will not work properly (see #603).\nIncremental completion on zsh：增强的实时自动命令补全插件：Incremental completion on zsh\n*该插件对性能似乎有一点点影响，请根据需要启用。*其实使用默认的自动补齐（tab）够用了。\n作用如图：\n​\t配置别名\nZsh 的 alias 配置项可以自定义命令别名，在使用一些比较复杂的命令时，使用别名可以提高效率，这里举例添加一个 Git 日志的别名：\n1 alias gli=\u0026#34;git log --color --graph --pretty=format:\u0026#39;%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)\u0026lt;%an\u0026gt;%Creset\u0026#39; --abbrev-commit\u0026#34; 注意等号两边不要有空格\nShell使用 在不同的终端中我们可以指定不同的Shell来使用。如：\n在Linux安装zsh中我们就指定了Linux终端的默认Shell为zsh。 在稍后介绍的[Windows Terminal](#Windows Terminal)中，也会指定Windows Terminal终端的默认Shell为Git bash。 下面介绍在IDE终端中设置自己的Shell。 设置IDE终端的Shell为git bash 如果你按照上面的教程成功在windows上安装了zsh，那么设置Shell为git bash，也就是设置为zsh。\n注意：没有安装zsh的情况下也能设置为git bash。优点：高亮、个人爱好。\n这里以goland开发工具为例，jetBrains全系列差不多：\n进入设置找到终端\nShell path为git安装路径里面的bin\\bash.exe,注意不是git-bash\n注意：还需要为bash.exe命令指定启动参数--login -i：\n图中没有加入，后期发现的问题。\n意思是启动登录Shell，并且交互运行。\n作用：这个环境下git bash不会出现乱码，文件、目录高亮。\n最后应用即可。\n其他类似的IDE配置差不多：\nvscode vscode默认为git bash设置了启动参数，很方便。\nTerminal使用 Windows Terminal Windows Terminal可以理解为：是cmd窗口和PowerShell终端的增强版，它将windows环境下的终端（cmd、PowerShell）都集成在了一起。我们只需要指定对应的Shell（cmd、PowerShell、git bash），即可使用。官方文档。\n主要特性：\n多选项卡支持： 一个窗口管理多个Shell。 丰富的自定义选项： 提供了多种自定义选项，包括更改背景颜色、字体、透明度、主题等，允许用户根据个人喜好定制界面。 同一套配置应用于不同Shell。 GPU 加速的文本渲染： 利用 DirectWrite 和 DirectX 提供 GPU 加速的文本渲染，确保快速、流畅的文本显示和滚动体验。 支持多种命令行工具和 Shell： 支持 PowerShell、CMD（Command Prompt）、Windows Subsystem for Linux（WSL）以及任何其他你喜欢的命令行工具。 Unicode 和 UTF-8 字符支持: 完整支持 Unicode 和 UTF-8 字符集，包括 emoji 和复杂字符。 JSON 配置文件： 使用 JSON 文件进行配置，允许用户灵活地定义设置，如启动命令、默认 Shell、外观等。 当然也提供了图形化界面修改配置(没有需要在Microsoft Store中升级到最新版)。 可扩展性和第三方工具支持： 可以通过扩展和插件进一步增强功能，支持第三方工具集成。 安装 Win11系统自带Windows Terminal终端，并且从Windows 11 22H2 版本开始，Windows Terminal 将正式成为 Windows 11 的默认终端。\n对于Win10系统需要额外安装，最低要求是Windows 10 2004（内部版本 19041）或更高版本。\n安装方法：\n推荐从Microsoft Store中下载安装。会自动更新。 如果无法从 Microsoft Store 安装 Windows Terminal，可以从github仓库的发布页面手动下载已发布的版本。 可以下载MSIX Bundle 文件，双击安装。这种方式不能指定安装位置。 或者根据自己的操作系统选择对应的压缩包，一般x64。 解压到哪里就安装到哪里，需要手动配置path环境变量，值为安装目录。 启动：在运行框中输入wt，能正常启动，证明安装成功。\n在安装成功之后，建议将Windows Terminal设置为默认的终端应用程序。由Windows决定，一般启动的也是wt。\n推荐文章：Windows终端（windows terminal）从下载到运行\n修改默认Shell为Git Bash 可以根据自己的爱好判断是否修改。\n每次打开 Windows Terminal 默认使用的是 Windows PowerShell，要改为默认使用 Git Bash，在设置里面进行设置即可。在更多选项中点击设置，或者右键标题栏空白处再点击设置。\n滚动到底部，点击添加新配置文件——新建空配置文件，然后填入你的git bash所在位置。示例：\n注意：\n指定的git bash是bin目录下的bash.exe。\n并且需要指定启动参数--login -i\n意思是启动登录Shell，并且交互运行。\n作用：这个环境下git bash不会出现乱码，文件、目录高亮。\n最后，在启动选项卡中设置 Git Bash 为默认终端并保存配置：\n美化 我在windows环境下使用的Shell一般是git bash，可定制化并不高，配置的zsh也没有使用，不过对我来说够用了。\n效果如下：\n这里就不详细介绍如何实现的了，Windows Terminal都提供了图形化配置。\n我遇到的问题：总结一下：Windows terminal集成git-bash，删除到头的时候窗口总是闪烁：\n解决办法：新建一个~/.inputrc 文件，输入set bell-style none（可以什么都不写也起作用），保存；重启terminal，问题解决。\n.inputrc是控制命令行界面（终端）行为的配置文件。\n神奇的是我只要创建了这个空文件，Windows terminal的git bash，删除到头之后就不会闪烁了。\nset bell-style none：关闭终端提示音。 set bell-style visible：使用可见提示而不是声音。 set editing-mode vi：将编辑模式设置为 Vi 模式。默认为： Emacs 模式。\n终端的编辑模式主要有vi和emacs两种模式，区别就是快捷键不同。\n如果你会vim，强烈建议修改为vi模式。\n推荐文章：vim-common-commands\n修改之后在insert模式下Emacs模式的部分快捷键依然可以使用。\nEmacs模式常用命令：\n注意：不同终端部分快捷键可用。\n光标移动： Ctrl + A：移动到行首 Ctrl + E：移动到行尾 Ctrl + B：向左移动一个字符 Ctrl + F：向右移动一个字符 Alt + B：向左移动一个词 Alt + F：向右移动一个词 文本编辑： Ctrl + K：删除从光标位置到行尾的文本 Ctrl + U：删除从光标位置到行首的文本 Ctrl + W：删除光标位置之前的一个词 Alt + D：删除光标位置之后的一个词 Ctrl + Y：粘贴（恢复）上次删除的文本 其他操作： Ctrl + L：清屏并重新显示当前行 Ctrl + R：搜索命令历史 esc+backspace：删除一个单词。 ctrl+P：上一条历史命令 ctrl+n:下一条历史命令 ctrl+j：回车 这里就不详细介绍控制终端行为的选项了。能配置终端的编辑模式、提示音、补缺行为、快捷键等。\n此外如果你喜欢使用PowerShell也可以使用Oh My Posh美化它，跟前面介绍的Oh My Zsh差不多。\n推荐文章：Oh My Posh | Windows Terminal 美化指南\nWindTerm WindTerm是一款跨平台的终端应用，同时也是 SSH/Telnet/Serial/Shell/Sftp 客户端。通常用于远程连接。类似于FinalShell，它有的功能，WindTerm同样支持，并且拥有更多的配色方案，直接上图：\n安装 WindTerm是一款部分开源的软件，现目前(2024/5/28)仍存在部分小问题，不过不影响正常使用。\n安装可以从github仓库的发布页面根据自己的操作系统手动下载已发布的版本。\n使用 使用WindTerm也很简单，语言支持中文。花点时间就能学会。\nWindTerm在windows环境下建议将默认的shell修改为git bash。个人爱好。\nWindows Terminal和WindTerm两款终端应用都建议安装，各有各的优点和用图。不过我一般使用WindTerm多一点，因为Windows Terminal有的功能WindTerm都有，并且后者有更好的配色方案，支持文件传输，内置远程连接。\n安装Windows Terminal主要是因为Windows Terminal集成了Win10的cmd、PowerShell终端。\n参考 原文地址: https://juejin.cn/post/7229507721795993661 ","date":"2024-05-27T15:17:13+08:00","permalink":"https://arlettebrook.github.io/p/terminal-and-shell-introduction/","title":"Terminal And Shell Introduction"},{"content":" 简介 Vim是一个高度可配置的文本编辑器，主要用于编写和编辑文本和源代码。它最初由Bram Moolenaar在1991年发布，至今已经成为了许多程序员和系统管理员的首选工具。\nVim的特点包括：\n高度可配置：Vim具有大量的命令和选项，可以根据用户的需求进行定制。\n命令模式（COMMAND)：Vim在默认情况下处于命令模式，用户需要通过键盘输入命令来进行文本编辑。\n插入模式(INSERT)：在插入模式下，用户可以输入文本内容。\n普通模式(NORMAL)：普通模式是Vim的默认模式，可以进行光标移动、删除字符等操作。\n（补充）可视模式（VISUAL)：允许用户以可视方式选择和操作文本。Visual 模式有三种类型：\n字符模式（Visual mode）：用于选择字符。v命令进入。 行模式（Visual Line mode）：用于选择整行。V命令进入。 块模式（Visual Block mode）：用于选择文本块（矩形区域）。ctrl+v命令进入。 可以与p, y, d, c, r, \u0026lt;, \u0026gt;, ~, gU, gu等命令组合使用。 大量快捷键：Vim具有大量的快捷键，可以提高编辑效率。\n宏记录：Vim支持宏记录，可以录制一系列键盘操作并重复执行。\n插件支持：Vim支持大量的插件，可以扩展其功能。\n在Linux和Unix系统中，Vim通常已经预装。Windows系统则需要手动安装。对于初次使用Vim的用户，建议先学习一些基础操作命令，并通过互联网上的资源来深入了解它的使用方法和高级功能。\n提示：\n阅读本文需要有一定的vim了解，还要拥有vim环境，并且你应该一边阅读，一边实操。\n如果你觉得本教程啰嗦，你可以运行vimtutor学习官方提供的tutor教程。有的vim版本是中文教程。如果你的不是可以网上搜索。备用(1.7中文版）（下载之后用vim打开阅读）。\n当然强烈建议你学习完本教程之后去过一边vimtutor，它可以让你边学边练。\n史上最好用的文本编辑器VIM 对于vi/vim只是点评一点：这是一个你不需要使用鼠标，不需使用小键盘，只需要使用大键盘就可以完成很多复杂功能文本编辑的编辑器。不然主流IDE也不会有vim插件，如：jetbrains全系列都支持的ideavim插件。\n学习 vim 并且其会成为你最后一个使用的文本编辑器。没有比这个更好的文本编辑器了，非常地难学，但是却不可思议地好用。\nvim的学习曲线相当的大（参看各种文本编辑器的学习曲线），所以，如果你一开始看到的是一大堆VIM的命令分类，你一定会对这个编辑器失去兴趣的。\n下面的文章翻译自《Learn Vim Progressively》，我觉得这是给新手最好的VIM的升级教程了，没有列举所有的命令，只是列举了那些最有用的命令。非常不错。\n警告：\n学习vim在开始时是痛苦的。 需要时间 需要不断地练习，就像你学习一个乐器一样。 不要期望你能在3天内把vim练得比别的编辑器更有效率。 事实上，你需要2周时间的苦练，而不是3天。 将常用命令总结为四个步骤：\n存活 感觉良好 觉得更好，更强，更快 使用VIM的超能力 当你走完这篇文章，你会成为一个vim的 superstar。\n——————————正文开始—————————— vim常用命令总结 第一级 – 存活 在Normal模式下【用vim正常打开的文件都会进入该普通模式】的常用命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 i #进入Insert 模式，按 ESC 回到 Normal 模式，光标处插入 # I: 行首插入 a #进入Insert 模式，按 ESC 回到 Normal 模式，光标后插入 # A：行尾插入 x #删除光标后的一个字符，并将删除的存到剪切板里 # X：删除光标前的一个字符，并将删除的存到剪切板里 dd #删除光标所在行，并将删除的存到剪切板里 # D：删除光标后面的所有字符，并将删除的存到剪切板里 p #粘贴剪切板，在光标后粘贴，P：光标处粘贴。 :q！ # 强制退出vim。 esc # 退出命令、回到normal模式。输入的命令没效果，就点它，直到起作用为止。与ctrl+c等效。 #推荐 hjkl #强例推荐使用其移动光标，但不必需，你也可以使用光标键 (←↓↑→). 注: j 就像下箭头。 注意：\n块光标，所在的位置由头部决定。头部就是光标所在位置。\n光标后是指隔一个字符，就是块光标的尾部。\n删除命令都会将删除的存到剪切板里，但只有vim命令删除的，粘贴p命令才能粘贴（与vim版本有关）。\n如果要粘贴系统的剪切板内容用shift+insert：将在光标处粘贴系统剪切板内容。 其他可能终端需要进入INSERT模式。 注意不同的终端启动不同的vim版本，p粘贴命令效果可能不一样。删除命令有的会进剪切板，有的不会，但是p能粘贴。 我的环境：ideavim插件。 补充：（后面学习之后知道：粘贴效果是与默认寄存器有关系） 粘贴命令在粘贴一行内容时（ideavim）：\n如果光标所在位置是空行，将粘贴到该行。p与P效果一样 如果光标所以位置不为空行，p将粘贴到下一行，P将粘贴到上一行。 也就是以行为单位：p：在光标后粘贴，P：光标处粘贴。 Windows版本vim粘贴命令行，无论是不是空行，p会粘贴到下一行，P会粘贴到上一行。\n你能在vim幸存下来只需要上述的那几个命令，你就可以编辑文本了，你一定要把这些命令练成一种下意识的状态。于是你就可以开始进阶到第二级了。\n在命令模式【输入:进入该模式，输入的命令都需要敲回车】下的常用命令（扩展）：\n1 2 3 4 5 6 # 使用vim提供的在线帮助系统 :help \u0026lt;command\u0026gt; # 显示相关命令的帮助。你也可以就输入 :help 而不跟命令。或者键盘上的HELP。或者F1。 # 如果打开的是vim内置的帮助文档，可以用ctrl+w w在文档和vim编辑窗口之间跳转。不起作用多点几下。其实是分屏切换快捷键。后续介绍。 # 选中帮助文档:q退出。选择编辑窗口:q!将会关闭所有窗口 :q # 只退出命令，可以退出未修改的文件、退出帮助等退出作用。 第二级 – 感觉良好 vim常用命令（难度一下就上来了）：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 #1.各种插入模式 o #在光标所在行后插入一个空行，从Normal模式进入Insert模式。 O #在光标所在行前插入一个空行，从Normal模式进入Insert模式。 cw #向后剪切光标所在字符的同类型字符。可以是一个字符串，或者一串标点符号，并从Normal模式进入Insert模式。 s # 该命令与x命令功能一样，区别：会进入insert模式 #2.简单的移动光标 0 #数字0到所在行头 $ #到所在行尾 ^ #到所在行首不是空白的地方 g_ #到所在行尾不是空白的地方 # 匹配搜索跳转\t/pattern #回车搜索 pattern 的字符串,如果搜索出多个匹配，可按n键到下一个，N上一个。匹配之后一直存在。 ?pattern # 逆向搜索跳转到指定字符，ctrl+o相对于大写的N :set hls # 开启高亮显示：have light search。默认关闭 :set hlsearch # 开启高亮显示 :set nohls # 关闭高亮显示 :nohlsearch # 临时关闭高亮显示 :nohls #临时关闭高亮显示，在次搜索或者n会出现高亮显示 :set incsearch # 动态显示搜索结果，通常配合高亮使用。默认关闭 :set is # 动态显示搜索结果，通常配合高亮使用。 :set noincsearch # 关闭动态显示搜索结果，通常配合高亮使用。 :set nois # 关闭动态显示搜索结果，通常配合高亮使用。 :set hls is :set nohls nois # 可以将set命令保存到vim的配置文件中让其永久生效。 # 匹配的时候：默认没有有忽略大小写。 :set ic # ignore case :set noic # no ignore case 不忽略大小写 /搜索的字符\\c # 临时忽略大小写 #3.拷贝/粘贴 p #光标后粘贴 P #光标出粘贴 yy #复制光标所在行 Y # 与yy等效。 #4.撤销/恢复 u #撤销 相对于ctrl+z ctrl+r # 恢复撤销，相对于ctrl+shift+z #5.打开/保存/退出/另存为/清空 :e \u0026lt;path/to/file\u0026gt; #打开指定文件，不存在会创建，e是edit的缩写。ideavim版本不存在打不开。 :e! # 放弃所有的更改，重新加载当前文件。 :bn #你可以同时打开很多文件，使用这两个命令来切换下一个或上一个文件 :bp #上一个文件 #冒号后可以加数字，:2bn表示下2个文件 :w #保存，w死write的缩写 #指定文件名就是另存为，后面加!: 强制保存 :sav \u0026lt;path/to/file\u0026gt; #二者区别：后者将修改保存到指定文件，并打开指定文件，可以继续编辑 # 旧文件的修改会被抛弃掉，sav是saveas的缩写。 :wq #保存退出，随便那个都行。 :wq! # 强制保存并退出 :wa # 保存所有打开的文件 :wqa # 保存所有打开的文件并退出 # 后面加！都是强制 :x #保存退出 ZZ #保存退出，效果一样 zz # 如果可能，将该行水平居中。等于与M。m是标记。 :q #退出 :q! #强制退出 :qa! #强行退出所有的正在编辑的文件，就算别的文件有更改。 :%d #删除文件所有内容 很好，花点时间熟悉一下上面的命令，一旦你掌握他们了，你就几乎可以干其它编辑器都能干的事了。但是到现在为止，你还是觉得使用vim还是有点笨拙，不过没关系，你可以进阶到第三级了。\n注意：\nc命令是一个组合命令，与d命令类似，都是删除命令，区别是c命令使用之后会进入insert模式：\nC与D命令一样，从光标位置删除到行尾部，区别D不会进入insert模式。 cc清空光标所在行并进入insert，与dd区别：不会删除该行，只是清空到剪切板。 w光标移动到不同类字符的头部（对空格字符会忽略，就是移动到下一个单词的开头）， 有大写命令，移动字符范围更广。就是对同类型字符判断更广。 cw从光标处删除到不同类字符的头部（不会忽略空格字符）。dw会忽略空格。 推荐使用de。 ciw删除光标所在的同类字符，并进入insert模式。diw不会进入insert模式。 caw删除光标所在的同类字符，并删除后面的所以空格。 这种类型，都是删除一个单词，但是不能与e组合。如cae。三个的不能与e组合，用w。 e移动到不同类字符的尾部（忽略空格字符），如果自身不在同类字符的尾部会先移动到尾部，在往下跳转。 注意：尾部是指光标在同类字符的最后一个单词前面。 ce删除单词并进入insert模式。 c0从光标位置删除到行头部。 c$从光标位置删除到行尾部。 当然也可以组合c^, cg_。 ct字符删除光标到第一个指定字符之间的字符（删除到指定的字符），并进入insert模式。 %会在最近括号之间来回跳转。 d%光标在括号上，会删除括号及括号内的内容。 意味着d命令也可以和上面组合，区别就是不会进入insert模式。 前面加数字表示重复几次。 2w光标移动两次不同类字符。注意w命令没有其他命令组合时会忽略空格。加数字也会忽略。 2dd类型删除两行。2cc删除之后进入insert模式。 y命令也是一个组合命令，用于与复制相关的命令。需要配合其他命令使用，如：：\nyy复制光标所在行。 3yy表示连续复制三行。 yw复制到不同类字符的前面，不会忽略空格。空格属于字符（就是复制一个单词）。 yiw复制所在的字符，就是复制完整单词。 yaw复制完整单词包括单词后面的空格，不包括前面的，不能与e组合。 y0: 从光标位置复制到行头，y^不包括空格。 y$: 从光标位置复制到行尾，yg_不包括空格。 v进入可视模式之后移动光标，选择文本之后，y命令可以复制选中的文本，自动返回normal。c会进入insert模式。 意味着前面的组合命令都可以与visual模式配合使用。 yt字符从光标位置复制到指定字符之间的字符。 y%如果光标在括号上，将复制括号及括号内的内容。 u命令相对于ctrl+z撤销命令：\nu命令会将光标移动到最近修改的地方，如果光标所在位置正是最近修改的地方，将执行撤销操作。(与vim版本有关，有的直接撤销)\nU撤销对当前行的所有更改,以行为单位，撤销到insert之前的状态。部分vim版本不能使用。\nctrl+u：\nnormal模式下：光标向上移动半页。 insert模式下：与Emacs模式一样。 command模式下：与Emacs模式一样，但是不会删除:。 第三级 – 更好，更强，更快 先恭喜你！你干的很不错。我们可以开始一些更为有趣的事了。\n这里你将会学到以下常用vim常用命令：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #1.重复命令【更好】 . #重复上一次命令 N\u0026lt;command\u0026gt; #数字N重复后面的命令N词 #演示： 2dd #删除两行 3p #粘贴剪切板3次 10iide [Esc] #光标所在字符前插入10个ide字符串，可以换成a就是后面，效果更好 . #重复上一次插入10ide命令 3. #插入3个ide，这里不是30个ide，要注意。 #2.光标移动【更强】：你要让你的光标移动更有效率，你一定要了解下面的这些命令，千万别跳过。 :N #光标到数字N行 NG #同理光标到数字N行，等同于Ngg ctrl+g # 显示文件的信息，包括行号，百分比，部分vim版本起作用。 gg #光标到第一行 G #光标到最后一行 w #光标移动到下一个不同类型的字符的开头，就是移动到不同类型的字符前，忽略空格字符。就是跳转到下一个单词前。 e #光标移动到 当前 或者 下一个 不同类型的最后一个字符上。效果：移动到当前同类型字符的末尾 或者 移动到下一个不同类型字符的末尾。【跳过单词】忽略空格字符。就是跳转到本单词或下一个单词末尾。 # 他们的大写命令，移动范围更广，就是对同类型字符判断更广。 #最强的光标移动 % #匹配括号移动，自己体会，超级好用。支持(, {, [。 * # 匹配光标当前所在的单词，移动到下一个单词 \\# # 匹配光标当前所在的单词，移动到上一个单词，实际\\没有，这里是转义作用。 #二者匹配之后都可以重复使用，起到切换的作用，n下一个同理 #更快 \u0026lt;start position\u0026gt;\u0026lt;command\u0026gt;\u0026lt;end position\u0026gt; #光标的移动与命令的联动，组合命令通常与光标移动命令组合使用，官方解释是操作加动作。 #演示 0y$ #拷贝当前行，一般用yy，0行首，y拷贝，$行尾 ye #拷贝当前同类型字符的末尾，y拷贝，e跳到同类型字符末尾，与yw效果一样 # 注意这两个要考虑光标的位置，光标必须在单词的头部。 # 用yiw,无论光标在哪里，只要在单词上，就能复制整个单词。 # yaw复制完整单词包括单词后面的空格，不包括前面的，不能与e组合。 dw #删除当前同类型字符 d$ #从当前删除到行尾 d0 #从当前删除到行首 dgg #从当前删除到文件开头 dG #从当前删除到文件末尾 :%d # 清空整个文件内容。 :e! # 忽略所有修改，重新加载当前文件。 #更多可组合命令d (删除 )、v (可视化的选择)、gU (变大写)、gu (变小写)、y（复制）、c（删除进入insert）等等 # 示例： y2/apple # 复制两个apple之间的内容，不包括最后一个apple。 gUiw # 将光标所在单词转大写。 diw # 删除当前光标所在单词 #d上面演示了 #可视化选择是一个很有意思的命令，你可以先按v，然后移动光标，你就会看到文本被选择，然后，你可能d，也可y，也可以变大写等 #注意变大小写要有对象才能变，不然出错。 补充（这部分可以最后在看，或看vimtutor在来）：\nr字符：替换光标后的字符为指定字符。\nR连续替换光标后的字符。esc退出。 :r 输入流可以提取文件（指定路径），或者输入流的内容到光标位置后面，如：:r !ls：将在光标后插入ls命令的输出。 用于提取文件内容到光标后 输入 :s/old/new/g 可以替换光标所在行的old字符串为new字符串。g表示替换区全部，没有只替换第一个\n要替换两行之间出现的每个匹配串，请 输入 :#,#s/old/new/g 其中 #,# 代表的是替换操作的若干行中首尾两行的行号。 输入 :%s/old/new/g 则是替换整个文件中的每个匹配串。 输入 :%s/old/new/gc 会找到整个文件中的每个匹配串，并且对每个匹配串提示是否进行替换。\n:!外部命令执行外部命令，如:!ls，支持所有的外部命令。\nvim支持保存一个文件的部分内容：\n进入可视模式之后，选中要保存的部分，然后按 : 字符。将看到屏幕底部会出现 :\u0026rsquo;\u0026lt;,\u0026rsquo;\u0026gt; 。在后面输入另存为文件命令即可w 文件名。 在命令模式下，可以使用tab键补全命令，ctrl+d显示能补全的命令。\nctrl+u清空光标前的字符，除了:。 f字符光标向前移动到指定字符前。F字符反向。\nt字符光标向前移动到指定字符的前一个字符前。T字符反向。 在使用这个两个命令之后;光标跳转到下一个相同字符，,上一个。 gf将光标所在行文本内容作为文件打开，会自动光标当前文件（前提已经保存）。\nH光标移动到光标去过的屏幕最高行，L屏幕的最低行。（与版本有关）\nJ删除末尾的换行符，ctrl-j换行、回车的意思，（重要）寄存器中用^J表示。 K查看光标所在单词的man页面。（ideavim不支持） b与w命令反向。B范围更广。\nge与e命令反向。 ctrl+unormal模式下光标向上移动半页。\nctrl+b光标向上移动差不多一页。\nctrl+f向下移动一页。\nctrl+d向下移动半页。\nM光标移动到屏幕中间。\n(向上移动到句子的开头\n)向下移动到句子的开头 {向上移动到段落的开头。\n}向下移动到段落的开头。 da(删除括号及括号内的内容。括号可以换成\u0026quot;, ', (, ), {, }, [, ].中的一个，删除对应的括号。\ndi(删除不包括括号。括号可以换成\u0026quot;, ', (, ), {, }, [, ].中的一个，删除对应的括号内容。\ni不包括括号，a包括括号。\n\u0026gt;\u0026gt;向后缩进光标所在行。\n\u0026lt;\u0026lt;反向缩进 .命令重复上一次命令。（部分命令不可重复：如2j) gU移动光标命令选中的字符转大写。\ngu移动光标命令选中的字符转小写 ~将光标后的字符大小写互转，可以选中多个互转。 在insert模式下ctrl-o会临时进入Normal模式，执行别的命令之后会，自动返回insert模式。\n可用于删除光标后的单词继续编辑：如：ctrl-o de vi模式中不可以使用。 在insert模式下：\nctrl-w向光标前删除到同类字符。忽略空格，就是删除一个单词。\nctrl-u全部删除。 ctrl-h等同于退格键。\nvim在insert模式下没有提供向后删除字符的命令\n第四级 – Vim 超能力 你只需要掌握前面的命令，你就可以很舒服的使用VIM了。但是，现在，我们向你介绍的是VIM杀手级的功能。下面这些功能是我只用vim的原因。\n其实是对上面的命令再次复习😂（不在解释其作用，如果你还看不懂，证明练得还不够。）：\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 0 ^ $ g_ f= # F , or ; t， # T , or ; 3f, dt, ct, df; cf; ye yy yt; yf; dw dd dt; df; cc ce cf; ct; p P diw daw ciw caw yiw yaw # 如果你搞不明白上面的复杂组合命令，可以看v模式选择的是那个部分。 viw vaw 补充：\n\u0026lt;motion\u0026gt;i\u0026lt;option\u0026gt;、\u0026lt;motion\u0026gt;a\u0026lt;object\u0026gt;根据动作（motion）、范围（i、a）操作对象（object）。\nmotion动作：常用：d, c, v, y,\ni与a的区别：\n对于空格i会忽略，a会保留后面的使用空格。 其他字符：i不包括，a包括。 object：操作对象，常用：\nw：一个单词。 s：一个句子（不包括缩进、前后空白）。 p：一个段落（包括缩进，前后空白）。 其他字符：\u0026quot;, ', (, ), {, }, [, ]. 注意：是向后搜索包裹光标的对象。没有，只有\u0026quot;对象会向内查找。\n示例：假设你有一个字符串 (map (+) (\u0026quot;foo\u0026quot;)).\n而光标键在第一个 o 的位置：\nvi\u0026quot; → 会选择 foo.\nva\u0026quot; → 会选择 \u0026quot;foo\u0026quot;. vi) → 会选择 \u0026quot;foo\u0026quot;.\nva( → 会选择(\u0026quot;foo\u0026quot;). v2i) → 会选择 map (+) (\u0026quot;foo\u0026quot;).\nv2a) → 会选择 (map (+) (\u0026quot;foo\u0026quot;)). 光标在a位置：\nvi\u0026quot; → 会选择 foo.\nva\u0026quot; → 会选择 \u0026quot;foo\u0026quot;.\nvi) → 会选择 map (+) (\u0026quot;foo\u0026quot;).\nva( → 会选择 (map (+) (\u0026quot;foo\u0026quot;)).\nv2i)、v2a)两个无法选中，不会向内查找。\n只有\u0026quot;会。并不考虑数量。v2i\u0026quot;→ 会选择 foo, 与vi\u0026quot;等效。 可视（VISUAL）模式操作：\nctrl+v进入可视模式的块模式（Windows占用，用ctrl+q代替）。v字符模式，V行模式。 以上模式可以配合其他移动动作使用，如0, ctrl+f , $方便选择。如： 选择多行，J命令批量移除选择行的换行符。 向后缩进\u0026gt;选中的文本，\u0026lt;向前。 选中之后=自动给缩进。 块模式还可以批量修改多行。典型的操作，如： 添加注释： 0 ctrl+v ctrl+d 选中要注释的 I # ESC 注释只能用行首插入。 末尾添加分号：$ ctrl+v ctrl+d 选择要修改的 A ; esc 注意：只有块模式可以批量修改。并且只有在块头或者块尾可以修改。 自动补全：\n在insert模式下，输入一个词的开头，然后按ctrl+norctrl+p自动补全就出现了。 如果有多个，n是下一个，p是上一个。 在ideavim中是代码提示。 选中之后，继续输入，不用回车确定。ideavim代码中需要回车确定。 具体看情况。 标记 在 Vim 中，m 命令用于设置标记（mark）。标记是 Vim 提供的一种功能，用于在文件中记住特定的位置，以便以后快速跳转到这些位置。标记可以是局部标记（仅在当前文件中有效）或全局标记（在所有文件中有效）。以下是 m 命令的详细使用方法：\n设置标记：\n设置局部标记： 操作：在普通模式下，按 m 然后按一个小写字母（例如 a）。 例子：按 ma 在当前光标位置设置标记 a。 设置全局标记： 操作：在普通模式下，按 m 然后按一个大写字母（例如 A）。 例子：按 mA 在当前光标位置设置全局标记 A。 在ideavim中会有高亮。 0-9字符也表示全局标记。 m命令是:mark 标记字符的快捷键。 区别是m会记录光标的列位置，:mark不会，它的列号始终为0。 所以:mark跳转只会到标记行的头部。不会跳转到标记行光标的位置。 一般使用m。 注意： 标记字符只有一个。 标记字符常用的字母、数字。其他的部分能用。不用考虑。 标记信息包括标记字符、光标的行号、列号以及行的文本内容。 后续设置存在的标记会覆盖。 跳转到标记：\n' 标记字符：自动跳转到标记行的行头。\n`` 标记字符`：自动跳转到标记行的光标位置。\n'a：跳转到局部标记a的行头。\n``A`跳转到全局标记A所在行光标的位置。\n跳转标记的开头如何有空格，都会忽略。\n查看标记：\n:marks：显示所有已设置的标记及其光标位置、行文本内容。 使用:mark标记的行，光标的列号为0。所以`` `也只能跳到标记行头。 删除标记：\n删除指定标记：:delmarks ...标记字符\n后面可以跟多个标记字符，用不用空格隔开都行。如： :delmarks ab 删除标记 a 和 b。 还可以删除范围内标记：如： :delmarks a-d 删除标记 a 到 d。 删除所有标记：:delmarks!\n注意该命令只能删除用户设置的所有本地标记，并且内置的特殊标记、全局标记不能删除。如', \u0026lt;, \u0026gt;。（我快崩溃了😭）\n上面的三个内置特殊标记，不建议用来做标记。他们具有特殊的含义。\n他们是动态生成的内置特殊标记，无法删除。\n含义如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026#39;\u0026lt; # 可表示上次visual模式选择的行头。还可表示标记的行头。 \u0026#39;\u0026gt; # 可表示上次visual模式选择的末尾的行头。还可以表示标记的行头。 `\u0026lt; # 可表示上次visual模式选择开始的位置。还可表示标记行鼠标的位置。 `\u0026gt; # 可表示上次visual模式选择的末尾。还可表示标记行鼠标的位置。 不用他们做标记。通常用在visual模式中，会动态自动生成。 例子： :\u0026#39;\u0026lt;,\u0026#39;\u0026gt;y 复制上次选择的内容。 :\u0026#39;\u0026lt;,\u0026#39;\u0026gt;d 删除上次选择的内容。 \u0026#39;\u0026#39; # 跳到光标上次所在的行头。 配合标记可以实现来回两个标记行头之间跳转 `` # 跳转到光标上次所在的确切位置（行和列）。 配合标记可以实现来回两个标记光标确切位置之间跳转 意味着：` \u0026#39; 做的标记会随时改变。不用他们做标记，会动态自动生成。 类似命令：\nCtrl-o：跳转到上一个位置。 Ctrl-i：跳转到下一个位置。 全局标记需要指定标记字符才能删除。\nVim 内置的特殊标记是 Vim 自动管理的，用于记录特定的编辑位置和选择范围。这些标记不能手动删除，因为它们是为特定功能设计的，并且在相应操作时会动态更新。用户可以删除自己设置的标记，但不能删除这些内置特殊标记。\n寄存器 在 Vim 中，寄存器（register）是一个临时存储位置，用于存储和检索文本、命令、宏等。寄存器的使用可以大大提高编辑效率。以下是对寄存器的详细介绍和使用方法：\n寄存器的分类：\nVim 中有多种类型的寄存器，每种类型都有特定的用途：\n无名寄存器（\u0026quot;）Unnamed Register： 默认寄存器，所有的删除（d）、复制（y）、粘贴（p）操作都与此寄存器关联。缓存最后一次操作内容。 具名寄存器（a 到 z 和 A 到 Z）Named Register： 用于存储用户指定的文本或宏。 小写字母（a 到 z）：覆盖存储。 大写字母（A 到 Z）：追加存储。 剪贴板寄存器（* 或 +）selection and drop： 用于与系统剪贴板交互。二者可以画等号，区别： *：与选择剪贴板（primary selection）交互（在 X Window 系统中）。 +：与系统剪贴板（clipboard）交互。 数字寄存器（0 到 9）numbered： 自动存储最近的删除文本。 0：最近的复制（yank）操作。 1 到 9：最近的删除（delete）操作，1 是最新的，9 是最旧的。 读取（命令）寄存器（:）： 存储最近的命令行输入。 搜索寄存器（/）last search pattern： 存储最近的搜索模式。 小删除寄存器（-）small delete： 用于存储小于一行的删除操作。 黑洞寄存器（_）black hole： 丢弃写入其中的任何内容，不对其进行存储。 表达式寄存器（=）expression： 允许你输入数学运算进行计算。 只能在Insert和Command模式使用，且部分版本不支持。 如：insert模式下：ctrl-r =2+2 enter结果为4。 只读寄存器read only： 命令寄存器: 点寄存器.：上次insert模式插入的文本内容。 搜索寄存器/ 当前文件名寄存器% 上一个文件名寄存器# 这些寄存器都只能读。 使用寄存器：\n查看寄存器内容：\n:reg 或 :registers：查看所有寄存器的内容。 例子：输入 :reg 查看所有寄存器的内容。 :reg {register}：查看指定寄存器的内容。 寄存器可以有多个，用不用空格隔开都行。:reg 01 2查看寄存器0, 1, 2的内容。 例子：输入 :reg a 查看寄存器 a 的内容。 引用寄存器：\n在Nomal模式下寄存器可以通过在名字前加双引号来引用。例如，我们可以通过 \u0026quot;a 来访问在 a 寄存器中的内容。 在Command或Insert模式下寄存器可以通过ctrl-r 名字来引用。 注意在这两个模式下引用的寄存器会立即输出其存储的内容。 复制到寄存器:\n\u0026quot;{register}y{motion}：复制文本到指定寄存器。 例子：输入 \u0026quot;ayiw 将当前单词复制到寄存器 a 中。 粘贴寄存器内容：\n\u0026quot;{register}p 或 \u0026quot;{register}P：粘贴寄存器内容。 p 在光标后粘贴，P 在光标前粘贴。 例子：输入 \u0026quot;ap 将寄存器 a 的内容粘贴到光标后。 删除到寄存器：\n\u0026quot;{register}d{motion}：删除文本并存储到指定寄存器。 例子：输入 \u0026quot;adiw 删除当前单词并将其存储到寄存器 a 中。 录制宏。\n注意事项：\n无名寄存器：是默认的寄存器。d、c、s、x，y，p这些操作的文本，都在\u0026quot;\u0026quot;无名寄存器中（存储最后一次操作内容）。除非修改默认寄存器。\n意味着删除的可以使用p命令粘贴，因为默认从无名寄存器取值。 默认寄存器如果是无名寄存器，p粘贴命令不能粘贴剪切板内容。要引用剪切板寄存器*或+才能粘贴。 自然在vim中复制、删除的也就不能进剪切板。需要指定剪切板寄存器。 数字寄存器：用于存储最近复制、删除的内容，删除的要以行为单位才会存储，否则删除的存储到-寄存器。复制的不影响。\n复制用0存储。复制的只有0和无名寄存器存储。后者很快失效。\n删除用1-9存储。数字越大，删除的时间越久。\n意味着指定寄存器，可以访问删除的内容: 1 \u0026#34;2p # 粘贴第二条删除的内容。 数字寄存器不可以被自定义的具名也就是字母寄存器替代。\n具名也就是字母寄存器存在的意义是固化需要频繁剪切的内容，不会因为操作频繁被覆盖。\n我们一般操作具名寄存器：复制，粘贴，删除。\n寄存器只可以覆盖，不可以删除，由vim自身管理生命周期。不是永久存在。\n可跨文件使用。\n寄存器的意义：\n首先，寄存器是可以跨文本文件使用的，只有有删除动作都会自动记录到1-9数字寄存器，0存储复制动作，而这些寄存器是动态的，会变化的，如果操作动作很多，有一些操作是需要频繁使用的，那么，使用自定义具名寄存器会大大的提高我们的效率，因为字母寄存器是不会改变的，除非你删除了这个寄存器。\n再次，我们如果有某些文件误修改了，或者有非法入侵，如果恶意闯入者没有删除所有寄存器的内容，我们有可能追踪到它的修改轨迹。\n最后，多说一句，寄存器的生命周期是很长的，即使服务器重启什么的，只要能正常进入系统，寄存器里的内容都是一直存在的。\nVim 的寄存器功能强大而灵活，通过熟练掌握寄存器的使用，可以极大地提升编辑效率。寄存器不仅可以用于基本的复制和粘贴，还可以存储宏、命令等，更好地管理和重用编辑内容。\n宏录制 在 Vim 中，宏是一组记录的按键序列，可以重复执行以自动化重复性的任务。以下是使用宏的详细步骤和相关命令：\n宏录制其实就是记录宏到寄存器（都是在普通模式下开始、结束）：\nq{register}：在Normal模式下，开始录制宏到指定寄存器。\n例子：按 qa 开始录制宏到寄存器 a 中。 开始录制之后可以任意执行vim命令。进行你希望录制的所有按键操作。Vim 会记录这些按键。\n例子：输入一系列编辑命令，如插入文本、删除文本、移动光标等。 q：结束录制宏。\n@{register}：执行存储在寄存器中的宏。\n例子：按 @a 执行寄存器 a 中的宏。 （ideavim目前不支持执行宏） @@：重复执行上一次执行的宏。\n多次执行宏：\n操作：在普通模式下，输入一个数字，然后按 @ 和寄存器字母键。 10@@执行10上次执行的宏。 例子：按 10@a 将宏 a 执行 10 次。 编辑和查看宏\n查看宏内容： 操作：在普通模式下，输入 :reg 查看所有寄存器的内容，包括宏。 例子：输入 :reg a 查看寄存器 a 的内容。 编辑宏内容： 操作：将宏内容复制到一个缓冲区进行编辑，然后将其粘贴回寄存器。 例子： 输入 :let @a='your edited macro'，将 'your edited macro' 替换为编辑后的宏内容。 或者，先用 :put a 将寄存器内容粘贴到缓冲区，编辑后用 :let @a=join(getline(1, \u0026quot;$\u0026quot;), \u0026quot;\\n\u0026quot;) 保存修改。 实例1：\n假设你想录制一个宏，用于在每行的末尾添加一个分号：\n开始录制： 按 qa 开始录制宏到寄存器 a。 进行操作： 按 $ 移动到行尾。 按 a; 在行尾插入分号。 按 Esc 退出插入模式。 按 j 移动到下一行。 结束录制： 按 q 结束宏录制。 执行宏： 按 @a 执行宏，在当前行末尾添加分号并移动到下一行。 多次执行宏： 按 10@a 执行宏 10 次，每次在行末尾添加分号并移动到下一行。 ctrl-a会对光标所在数字加1.\n示例2：\n在一个只有一行且这一行只有“1”的文本中，键入如下命令：\nqaYp\u0026lt;C-a\u0026gt;q\n解释：\nqa 开始录制。 Yp 复制行并粘贴。 \u0026lt;C-a\u0026gt; 增加1。 q 停止录制。 @a → 在1下面写下 2\n@@ → 在2 正面写下3\n现在做 100@@ 会创建新的100行，并把数据增加到 103.\n通过使用宏，你可以在 Vim 中有效地自动化重复性任务，提高编辑效率。录制、执行和编辑宏的灵活性使得它们成为强大的工具，适用于各种编辑场景。\n分屏 在 Vim 中，分屏（split screen）功能非常强大，允许你在同一个 Vim 会话中同时查看和编辑多个文件或同一个文件的不同部分。以下是 Vim 分屏操作的详细介绍：\n分类：\n水平分屏：命令：:sp [file]or:split [file]： 指定文件水平分屏，没有指定为当前文件。 快捷键：ctrl+w s 垂直分屏：命令：:vsp [file]or :vsplit [file] ： 指定文件重置分屏，没有指定默认为当前文件。 快捷键：ctrl+w v 操作（最后一个快捷键可以分开按）：\n分屏窗口跟普通窗口一模一样，可以执行vim的所有的命令。 分屏的窗口还可以继续分屏。 切换到其他窗口 ctrl+w w不同分屏窗口之间来回切换。 Ctrl-w h：切换到左边的窗口。 Ctrl-w j：切换到下面的窗口。 Ctrl-w k：切换到上面的窗口。 Ctrl-w l：切换到右边的窗口。 调整窗口高度（部分vim版本不起作用）： Ctrl-w +：增加当前窗口高度。 Ctrl-w -：减少当前窗口高度。 Ctrl-w =：使所有窗口等高。 ctrl-w _：最大化当前水平窗口。 ctrl-w |：最大化当前垂直窗口。 调整窗口宽度： Ctrl-w \u0026gt;：增加当前窗口宽度。 Ctrl-w \u0026lt;：减少当前窗口宽度。 关闭当前窗口： :q 或 :quit：关闭当前窗口。 快捷键：Ctrl-w c 或 Ctrl-w q。 关闭其他窗口： :only：关闭除了当前窗口之外的所有窗口。 快捷键：Ctrl-w o。 交换窗口位置： Ctrl-w r：旋转窗口布局。 拆分到标签页： :tab split：在新标签页中水平分屏当前文件。 :tab vsplit：在新标签页中垂直分屏当前文件。 我测试二者效果一样，就是将指定文件，默认当前文件拆分到标签页中。跟浏览器标签页类似。 vim打开多个文件，默认不会分屏，是以多窗口的方式打开，可以使用bn, bp切换。 -o[n]选项:指定打开的分屏窗口数量，默认n为1个，默认水平分屏。 可以不用指定n，根据后面的文件数来确定分屏。 如果n与文件数不等，n有几个就分几屏，多出的文件到分别一个完整的窗口中。 -O垂直分屏。后面有几个文件，就几个垂直分屏。（n与文件数相等的情况）。不等分屏数由n决定，多的文件分别单独一个窗口。 结束语 上面是作者最常用的90%的命令。\n我建议你每天都学1到2个新的命令。\n在两到三周后，你会感到vim的强大的。\n有时候，学习VIM就像是在死背一些东西。\n官方建议：要在使用中学习，而不是在记忆中学习。 幸运的是，vim有很多很不错的工具和优秀的文档。\n运行vimtutor直到你熟悉了那些基本命令。\n其实在线帮助文档中你应该要仔细阅读的是 :help usr_02.txt.\n你会学习到诸如 !， 目录，寄存器，插件等很多其它的功能。\n学习vim就像学弹钢琴一样，一旦学会，受益无穷。\n——————————正文结束—————————— 扩展 更多内容参考vim-introduction。\n目录操作 Vim 自带的 netrw 插件提供了文件浏览器功能，可以用来浏览、操作目录和文件。\n打开目录浏览：\n1 2 3 :Explore :Ex # 简写 会关闭当前文件。 用vim移动命令移动，回车打开文件或命令。 :q退出浏览。 在垂直分屏窗口中打开目录浏览器：\n1 2 3 :Vexplore :Vex # 简写 分屏命令都可以用。 在水平分屏窗口中打开目录浏览器：\n1 2 3 :Sexplore :Sex # 简写 浏览指定目录：\n1 2 3 :Explore /path/to/directory :Vex /path/to/directory 部分vim版本不带该目录浏览插件。\n参考 原文地址Vim简明教程【CoolShell】 vim的寄存器详解 (译)Vim 寄存器：由浅入深 ","date":"2024-05-27T00:43:37+08:00","permalink":"https://arlettebrook.github.io/p/vim-common-commands/","title":"Vim Common Commands"},{"content":" 模板渲染是指根据特定的模板语法，将这些模板渲染成文本，通常用于生成 HTML、邮件、配置文件等。在Go语言中提供了两个标准库来渲染模板，它们都具有相同的模板语法，分别是text/template 和 html/template ，接下来将介绍这两个标准库如何使用。\n快速使用 二者都是go语言的标准库，直接使用：\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 package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;text/template\u0026#34; //\u0026#34;html/template\u0026#34; ) func main() { const tmpl = \u0026#34;Hello, {{.Name}}! Your message is: {{.Message}}\u0026#34; data := struct { Name string Message string }{ Name: \u0026#34;World\u0026#34;, Message: \u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;XSS\u0026#39;);\u0026lt;/script\u0026gt;\u0026#34;, } t := template.New(\u0026#34;example\u0026#34;) t, err := t.Parse(tmpl) if err != nil { log.Fatalf(\u0026#34;Parsing template: %s\u0026#34;, err) } err = t.Execute(os.Stdout, data) if err != nil { log.Fatalf(\u0026#34;Excuting templete: %s\u0026#34;, err) } } 使用template库很简单，只需要创建Template对象（模板不是文件需要指定模板的名称），调Parse方法解析模板，最后调Execute方法指定输出的地方和数据，就能渲染模板。\n模板是一串字符串，由模板语法组成。模板语法都包含在{{和}}中间，其中{{.}}中的点表示当前对象。输出的地方是一个io.Writer。数据常用的是结构体对象或map类型。渲染模板是根据模板语法将数据绑定到模板中。\n上面程序运行输出：\n1 2 $ go run main.go Hello, World! Your message is: \u0026lt;script\u0026gt;alert(\u0026#39;XSS\u0026#39;);\u0026lt;/script\u0026gt; 注释掉\u0026quot;text/template\u0026quot;换成\u0026quot;html/template\u0026quot;输出：\n1 2 $ go run main.go Hello, World! Your message is: \u0026amp;lt;script\u0026amp;gt;alert(\u0026amp;#39;XSS\u0026amp;#39;);\u0026amp;lt;/script\u0026amp;gt; 二者的主要区别就是html/template会将渲染的HTML内容转义。意思就是让绑定的HTML数据变成普通文本，失去其原来的作用，能够防止跨站脚本攻击 (XSS)。因此当我们的模板是HTML时，推荐使用html/template包渲染，并且该包也是专门为渲染HTML模板准备的，确保生成的 HTML 是安全的。\ntemplate介绍 在 Go 语言中，text/template 和 html/template 都是用于模板处理的包，它们在功能上有许多相似之处，但也有重要的区别，尤其是在处理 HTML 时。\n作用：\ntext/template：适用于生成纯文本内容，比如邮件、日志、配置文件等。这些内容不涉及 HTML 安全性问题。 html/template：专门用于生成 HTML 内容，提供了防止 XSS 攻击的保护机制。 共同点:\n模板解析：两个包都使用类似的语法来解析模板，支持条件语句、循环、自定义函数等。 数据绑定：都可以将结构体、映射等数据绑定到模板中，从而生成动态内容。 模板执行：都使用 Execute 和 ExecuteTemplate 方法来执行模板，将结果输出到 io.Writer。 不同点：\n主要区别：\n在模板中插入用户提供的数据时，html/template 会自动对这些数据进行 HTML 转义，确保生成的 HTML 是安全的，能够防止跨站脚本攻击 (XSS)。而text/template包不会。 内置的模板函数同名，但功能可能不一样。\n跨站脚本攻击（XSS）：大概意思是脚本不是自己网站提供的，是别人恶意放到你网站上的。\n当用户访问你的网站时，恶意脚本就会运行。\n恶意脚本可能做的事：\n窃取 Cookie 和会话信息： 攻击者可以通过恶意脚本窃取用户的 Cookie 和会话信息，从而冒充用户进行操作。 劫持用户会话： 攻击者可以利用窃取的会话信息，劫持用户的会话，进行恶意操作。 伪造请求： 恶意脚本可以在用户不知情的情况下发起伪造请求，执行一些用户未授权的操作。 传播蠕虫： 恶意脚本可以通过 XSS 漏洞传播蠕虫，自动感染访问受害页面的用户。 虚假内容： 恶意脚本可以修改页面内容，显示虚假信息欺骗用户。 防御XSS攻击：\n输出编码：\n在将用户输入的数据输出到 HTML 内容时，进行 HTML 转义，防止恶意脚本注入。\nhtml/template包就会自动转义HTML内容。 在属性中输出数据时，对数据进行属性转义。\n输入验证：\n对用户输入的数据进行严格验证和过滤，只允许符合预期格式的数据通过。 使用安全的 JavaScript：\n避免直接使用 innerHTML，改用 textContent 或其他安全的 DOM 操作方法。 内容安全策略 (Content Security Policy, CSP)：\n配置 CSP 头，通过白名单机制限制允许执行的脚本来源，减少 XSS 攻击的风险。\n1 Content-Security-Policy: script-src \u0026#39;self\u0026#39; template使用 提前介绍模板语法中的定义模板：\n模板就是一串字符串或一个文本文件。\n当为一串字符串时，可能是主模板，也可能是子模板。\n没有定义子模板时，是主模板。反之就是子模板。 当为一个文本文件时，相对文件，文件名就是主模板。\n通常会将目录/文件名设为该文件的子模板，以表示主模板。防止不同目录出现同名文件。此时渲染整个文件，就需要指定子模板名。 定义子模板：用define内置模板函数，后面更子模板名，是字符串类型，用双引号括起来。结束用end函数标识。中间就是子模板的内容。如：\ntemplates/default/index.tmpl：\n1 2 3 {{ define \u0026#34;default/index.tmpl\u0026#34; }} Hello, {{ .Name }}! Your message is: {{ .Message }} {{ end }} 此时渲染这个文件，就需要用指定模板名的方法。\n更多内容参考。\n渲染Template对象 渲染Template对象就是渲染模板，用到的方法是func (t *Template) Execute(wr io.Writer, data any) error或func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error。\n二者的区别是：\nExecute方法用于渲染主模板。\n只有一个模板，这个模板就是主模板。 有多个模板时，定义或解析模板时最先创建的那个模板，或是执行时作为入口点的模板（没有指定模板名的都会先渲染），最先渲染的模板就是主模板。 ExecuteTemplate方法用于渲染指定模板名的模板。 总结：\n只有一个模板用Execute方法，也可以用ExecuteTemplate方法，当需要指定模板名。 有多个模板用ExecuteTemplate方法，需要指定模板名。如果此时用Execute方法将只渲染主模板。 用func (t *Template) DefinedTemplates() string方法可以查看定义的所以模板。返回的字符串格式为; defined templates are:... 创建Template对象 创建Template对象的方法主要有以下几种：\n解析并创建Template对象：\ntemplate.ParseFiles： 从文件中解析模板并创建Template对象，可以一次解析一个或多个文件。\n1 2 3 4 t, err := template.ParseFiles(\u0026#34;templates/file1.tmpl\u0026#34;, \u0026#34;templates/file2.tmpl\u0026#34;) if err != nil { // handle error } template.ParseGlob： 使用通配符模式解析一组模板文件并创建Template对象。（常用）\n所有模板都放在templates目录下（没有分类保存）：\n1 2 3 4 t, err := template.ParseGlob(\u0026#34;templates/*.tmpl\u0026#34;) if err != nil { // handle error } 所有模板都放在templates目录下并分类保存：\n需要多匹配一级分类目录，目录用/**/表示该级目录（只能匹配一级）。两级用/**/**，以此类推。如：\n1 2 3 4 t, err := template.ParseGlob(\u0026#34;templates/**/*.tmpl\u0026#34;) if err != nil { // handle error } 将匹配templates目录下的所有一级目录下的所有*.tmpl文件。\n分目录保存，为防止不同目录出现同文件名，造成模板丢失，通常会将目录/文件名设为该文件的子模板，以表示主模板。防止不同目录出现同名文件。此时渲染整个文件，就需要指定子模板名。\n创建空的命名模板对象：\ntemplate.New： 创建一个命名模板的基础模板对象，不会解析任何模板内容。 创建命名模板对象之后，还需要解析模板才能使用。 通常与Parse（常用）、ParseFiles、ParseGlob等方法一起使用。使用方法与同名函数一直。只要是Template对象都能使用这几个方法。 区别是： 用New方法创建空的命名模板后（这模板就是主模板），在用Parse等方法解析模板： 如果解析的模板没有命名且非空，则会覆盖主模板的内容。 如果解析的模板有命名，则会为主模板添加子模板。 以上情况为字符串模板。 解析文件都是为主模板添加子模板。因为文件名默认为一个模板名。 template.Must： 是一个帮助函数，用于包装解析函数（如Parse、ParseFiles、ParseGlob等）的返回值。如果解析过程中发生错误，它会导致程序在运行时崩溃。常用于简化创建模板对象代码。\n示例：\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 package main import ( \u0026#34;os\u0026#34; \u0026#34;text/template\u0026#34; ) func main() { // 创建一个新的模板并解析内容 tmpl := template.New(\u0026#34;example\u0026#34;) tmpl, err := tmpl.Parse(\u0026#34;Hello, {{.Name}}!\u0026#34;) if err != nil { panic(err) } // 直接解析模板字符串，使用帮助函数简化创建模板对象 tmpl = template.Must(template.New(\u0026#34;example\u0026#34;).Parse(\u0026#34;Hello, {{.Name}}!\u0026#34;)) // 从多个文件解析模板 tmpl, err = template.ParseFiles(\u0026#34;example1.tmpl\u0026#34;, \u0026#34;example2.tmpl\u0026#34;) if err != nil { panic(err) } // 使用通配符从文件夹解析模板 tmpl, err = template.ParseGlob(\u0026#34;templates/*.tmpl\u0026#34;) if err != nil { panic(err) } // 简化创建模板对象，解析失败会自动抛出panic tmpl = template.Must(template.ParseGlob(\u0026#34;templates/**/*.tmpl\u0026#34;)) // 执行模板 err = tmpl.Execute(os.Stdout, map[string]string{\u0026#34;Name\u0026#34;: \u0026#34;World\u0026#34;}) if err != nil { panic(err) } } 模板语法介绍 基础语法 模板语法都包含在{{和}}中间。{{.}}：点表示当前对象。\n数据是结构体对象时，直接用.属性名访问属性。\n数据是映射时，直接用.键名访问值。\n键名或属性名要一一对应。\n以上两个可以互相嵌套访问。示例：\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 package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;text/template\u0026#34; //\u0026#34;html/template\u0026#34; ) type User struct { Name string Age int Other map[string]any } func main() { t := template.Must(template.New(\u0026#34;example\u0026#34;).Parse(`{{ .title }} 姓名：{{ .user.Name }} 年龄：{{ .user.Age }} QQ: {{ .user.Other.QQ }} 学校：{{ .user.Other.school }}`)) err := t.Execute(os.Stdout, map[string]any{ \u0026#34;title\u0026#34;: \u0026#34;用户信息\u0026#34;, \u0026#34;user\u0026#34;: User{ Name: \u0026#34;Jack\u0026#34;, Age: 18, Other: map[string]any{ \u0026#34;QQ\u0026#34;: 9527, \u0026#34;school\u0026#34;: \u0026#34;野鸡大学\u0026#34;, }, }, }) if err != nil { log.Fatalf(\u0026#34;Excuting templete: %s\u0026#34;, err) } } 运行输出：\n1 2 3 4 5 6 $ go run main.go 用户信息 姓名：Jack 年龄：18 QQ: 9527 学校：野鸡大学 数据是基本数据类型时，如int、bool、string。直接用.表示值。\n数据是切片、数组、映射等集合类型时，需要遍历对象。（稍后介绍）\n注意映射有两种方法访问值：.键名或者循环遍历。 格式: {{ 模板表达式 }}。模板表达式与括号之间建议用空格隔开。如果有空格渲染时会自动移除。\n模板表达式可以是.、函数、变量等组成。\n表示式如果没有值，渲染后会用\u0026lt;no value\u0026gt;表示。\n在两个括号内添加-（与括号之间不能有空格，与表达式之间必须有空格）表示删除空白。\n删除的是渲染结果与周围字符之间的空白。 -在那边就表示删除那边与字符之间的空白。都有表示左右两边字符都删除。 空白指空格、换行。 1 2 \u0026#34;{{ 23 -}} \u0026lt; {{ 45 }}\u0026#34; // 输出23\u0026lt; 45 \u0026#34;{{ 23 -}} \u0026lt; {{- 45 }}\u0026#34; // 输出23\u0026lt;45 注释格式: {{/* 注释内容 */}}。注意/与括号之间不能有空格。执行时会忽略。可以多行。注释不能嵌套。*与注释内容可以没有空格。\n定义模板前面已经介绍了。\n补充：定义子模板语句如果独占一行，虽然渲染后为空，但是会独占一空行，注释也会，后面的定义变量也是。 为不影响渲染之后的结构，可以将他们与内容写在一行。这样即使注释换行也不会占一空行。模板语法与其他内容之间也不要有空格，否则渲染之后会保留空格。 后来才知道删除空格的作用。最优解决方案：使用用-可以删除空格、换行。能达到美观而不影响渲染结果。 模板变量\n在模版中可以自定义变量, 类似golang使用:=符号定义变量，用来保存传入模板的数据或其他语句生成的结果。语法为：{{ $变量名 := 数据 }}\n为存在的变量赋值{{ $变量名 = 数据 }}\n引用变量{{ $变量名 }}\n数据可以是字符串（用双引号括起来）、整型、表达式的值。\n引用变量可以与表达式组合使用。\n上面示例可修改为：\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 package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; //\u0026#34;text/template\u0026#34; \u0026#34;html/template\u0026#34; ) type User struct { Name string Age int Other map[string]any } func main() { t := template.Must(template.New(\u0026#34;example\u0026#34;). Parse(`{{ define \u0026#34;aaa\u0026#34; }} {{ .title }} {{ $other := .user.Other }} 姓名：{{ .user.Name }}{{/* 这是注释 换行了 */}} 年龄：{{ .user.Age }} QQ: {{ $other.QQ }} 学校：{{ $other.school }}{{ end }}`)) err := t.ExecuteTemplate(os.Stdout, \u0026#34;aaa\u0026#34;, map[string]any{ \u0026#34;title\u0026#34;: \u0026#34;用户信息\u0026#34;, \u0026#34;user\u0026#34;: User{ Name: \u0026#34;Jack\u0026#34;, Age: 18, Other: map[string]any{ \u0026#34;QQ\u0026#34;: 9527, \u0026#34;school\u0026#34;: \u0026#34;野鸡大学\u0026#34;, }, }, }) if err != nil { log.Fatalf(\u0026#34;Excuting templete: %s\u0026#34;, err) } } 运行输出：\n1 2 3 4 5 6 7 $ go run main.go 用户信息 姓名：Jack 年龄：18 QQ: 9527 学校：野鸡大学 可以对比一下有什么区别。（定义子模块的头部独占一空行，解决办法：与title同行且在前面）\n可以不用修改结构，使用-删除空白。推荐使用\n流程控制语句 介绍流程控制语句前，先介绍比较函数（也叫逻辑运算函数）。将逻辑运算封装成了函数形式。\n常用的比较函数如下：\neq：等于 ne：不等于 lt：小于 le：小于等于 gt：大于 ge：大于调用 比较函数后面跟两个可比较的参数，用空格分隔开。返回true或者false，对应类型的零值为false，其余为true。如：\n1 2 {{ lt 22 33 }} // 输出true {{ ge 22 33 }} // 输出false 比较函数可以与条件判断（if语句）组合使用。\n模版语法的流程控制语句主要指if/range/with三种语句。\n条件判断 if（ else, else if）语句用于根据条件来控制模板的输出。可以使用else和else if来处理其他情况。\n格式：\n1 2 3 4 5 6 7 {{if condition}} \u0026lt;!-- 当condition为true时输出的内容 --\u0026gt; {{else if otherCondition}} \u0026lt;!-- 当otherCondition为true时输出的内容 --\u0026gt; {{else}} \u0026lt;!-- 当condition和otherCondition都为false时输出的内容 --\u0026gt; {{end}} if语句后面跟end表示结束。\n示例：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package main import ( \u0026#34;bufio\u0026#34; \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;text/template\u0026#34; \u0026#34;github.com/spf13/cast\u0026#34; ) var ErrExit = errors.New(\u0026#34;exit\u0026#34;) const tmpl = `你的成绩：{{ if lt . 60 }}不及格{{ else if eq . 60 }}刚好及格{{ else }}及格了{{ end }}` func ReadScore() (int, error) { fmt.Print(\u0026#34;请输入你的成绩（q：退出）：\u0026#34;) r := bufio.NewReader(os.Stdin) s, err := r.ReadString(\u0026#39;\\n\u0026#39;) if err != nil { return 0, fmt.Errorf(\u0026#34;fail to read string: %w\u0026#34;, err) } if strings.Contains(strings.ToLower(s), \u0026#34;q\u0026#34;) { return 0, ErrExit } score, err := cast.ToIntE(strings.TrimSpace(s)) if err != nil { return 0, fmt.Errorf(\u0026#34;conversion to integer failed %w\u0026#34;, err) } return score, nil } func DisplayGrade(data int, t *template.Template) error { err := t.Execute(os.Stdout, data) if err != nil { return fmt.Errorf(\u0026#34;fail to execute template: %w\u0026#34;, err) } fmt.Println() return nil } func main() { t := template.Must(template.New(\u0026#34;example\u0026#34;).Parse(tmpl)) for { score, err := ReadScore() if err != nil { if errors.Is(err, ErrExit) { log.Println(\u0026#34;Exit successful\u0026#34;) return } log.Printf(\u0026#34;Fail to read score: %s\u0026#34;, err) continue } err = DisplayGrade(score, t) if err != nil { log.Printf(\u0026#34;Fail to diaplay grade: %s\u0026#34;, err) } } } 运行输出：\n1 2 3 4 5 6 7 8 9 $ go run main.go 请输入你的成绩（q：退出）：55 你的成绩：不及格 请输入你的成绩（q：退出）：60 你的成绩：刚好及格 请输入你的成绩（q：退出）：60 你的成绩：刚好及格 请输入你的成绩（q：退出）：q 2024/06/04 15:44:17 Exit successful 循环 循环range语句用于迭代数组、切片、映射等集合类型的数据。跟go语言的for-range差不多，甚至比go语言更简洁。\n格式：\n1 2 3 {{range 集合类型数据}} {{.}} {{end}} 遍历集合类型数据之后，可以用.访问每一个集合元素的值。\n注意：range范围内，.的作用域仅在range范围内，无法访问外部对象。\n如果需要访问索引，可以使用两个变量接收：\n1 2 3 4 5 {{range $index, $element := 集合类型数据}} Index: {{$index}}, Value: {{$element}} {{else}} // 看情况是否使用 没有数据可遍历 {{end}} range语句后面跟end表示结束。它们之间还可以嵌入else语句，当集合类型数据长度为0时将执行else语句。\n1 2 3 4 5 {{range 集合类型数据}} {{.}} {{else}} // 看情况是否使用 没有数据可遍历 {{end}} 与if语句组合使用：\n1 2 3 4 5 6 7 {{range .Items}} {{if .IsActive}} Active item: {{.Name}} {{end}} {{else}} No items found. {{end}} 示例1：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 {{define \u0026#34;userList\u0026#34;}} \u0026lt;ul\u0026gt; {{range .Users}} \u0026lt;li\u0026gt; {{if .Active}} Active user: {{.Name}} {{else}} Inactive user: {{.Name}} {{end}} \u0026lt;/li\u0026gt; {{else}} \u0026lt;li\u0026gt;No users found.\u0026lt;/li\u0026gt; {{end}} \u0026lt;/ul\u0026gt; {{end}} 示例2：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;html/template\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) func main() { const tmpl = `爱好：{{ range . }} {{ . }}{{ else }}没有任何爱好{{ end }}` t := template.Must(template.New(\u0026#34;example\u0026#34;).Parse(tmpl)) data := []string{\u0026#34;看电影\u0026#34;, \u0026#34;跑步\u0026#34;, \u0026#34;打篮球\u0026#34;, \u0026#34;看小说\u0026#34;} err := t.Execute(os.Stdout, data) assertExecErr := func(err error) { if err != nil { log.Fatalf(\u0026#34;Fail to execute template: %s\u0026#34;, err) } } assertExecErr(err) fmt.Println(\u0026#34;\\n----------------------\u0026#34;) err = t.Execute(os.Stdout, []string{}) assertExecErr(err) fmt.Println(\u0026#34;\\n----------------------\u0026#34;) const tmpl2 = `爱好2：{{ range $index, $value := . }} Index: {{ $index }}, Value: {{ $value }}{{ else }}没有任何爱好{{ end }}` t2 := template.Must(template.New(\u0026#34;example2\u0026#34;).Parse(tmpl2)) err = t2.Execute(os.Stdout, data) assertExecErr(err) fmt.Println(\u0026#34;\\n----------------------\u0026#34;) err = t2.Execute(os.Stdout, []string{}) assertExecErr(err) } 运行输出：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ go run main.go 爱好： 看电影 跑步 打篮球 看小说 ---------------------- 爱好：没有任何爱好 ---------------------- 爱好2： Index: 0, Value: 看电影 Index: 1, Value: 跑步 Index: 2, Value: 打篮球 Index: 3, Value: 看小说 ---------------------- 爱好2：没有任何爱好 变量声明（with, define, block） with with语句用于重定义模板的作用域。常用于缩短长的字段访问路径。就是可以缩短结构体对象属性的访问路径。简单理解：重定义.的作用域（var . := .User）：将当前对象的User属性复制给.。如：\n1 2 3 4 5 6 7 8 9 // 访问Data结构体下的user属性 Name: {{.User.Name}} Email: {{.User.Email}} // 使用with {{with .User}} Name: {{.Name}} Email: {{.Email}} {{end}} 注意：range和with语句都改变了点（.）引用的数据，那么如果想要在range和with语句中引用模版参数，请先将（点（.）赋值给一个自定义变量, 然后在range和with中通过自定义变量，引用模版参数。\n示例：\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 package main import ( \u0026#34;html/template\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) type User struct { Name string Email string } type Data struct { User } func main() { const tmpl = `用户信息：{{ with .User }} 姓名: {{ .Name }} 邮箱：{{ .Email }} {{ end }}` t := template.Must(template.New(\u0026#34;example\u0026#34;).Parse(tmpl)) data := Data{User{Name: \u0026#34;arlettebrook\u0026#34;, Email: \u0026#34;arlettebrook@proton.me\u0026#34;}} err := t.Execute(os.Stdout, data) if err != nil { log.Fatalf(\u0026#34;Fail to execute template: %s\u0026#34;, err) } } 运行输出：\n1 2 3 4 $ go run main.go 用户信息： 姓名: arlettebrook 邮箱：arlettebrook@proton.me define define语句用于定义一个模板，通常用于复用模板片段，实现模板嵌套。\n1 2 3 {{define \u0026#34;templateName\u0026#34;}} \u0026lt;!-- 模板内容 --\u0026gt; {{end}} 可以在其他地方使用template来引用：\n1 {{template \u0026#34;templateName\u0026#34; .}} template函数的第一个参数是模板名字，第二个参数是当前模板参数, 在子模板内部也是通过点( . )，引用模板参数。\n当子模板没有参数时，.是可选的。\n注意：\n引入子模板带参数的时候别忘记最后的.。用于传递模板参数。 定义子模版不能嵌套。 示例：\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 package main import ( \u0026#34;html/template\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) type User struct { Name string Email string } type Data struct { User } func main() { const info = `{{ define \u0026#34;userinfo.tmpl\u0026#34; }}{{ with .User }} 姓名: {{ .Name }} 邮箱：{{ .Email }}{{ end }}{{end}}` const tmpl = `用户信息：{{template \u0026#34;userinfo.tmpl\u0026#34; . }}` t := template.Must(template.New(\u0026#34;example\u0026#34;).Parse(tmpl)) t, err := t.Parse(info) if err != nil { log.Fatalf(\u0026#34;Fail to parse template: %s\u0026#34;, err) } data := Data{User{Name: \u0026#34;arlettebrook\u0026#34;, Email: \u0026#34;arlettebrook@proton.me\u0026#34;}} err = t.Execute(os.Stdout, data) if err != nil { log.Fatalf(\u0026#34;Fail to execute template: %s\u0026#34;, err) } } 运行输出：\n1 2 3 4 $ go run main.go 用户信息： 姓名: arlettebrook 邮箱：arlettebrook@proton.me 模板管理\n上面的例子，我们将模板代码定义在一个变量或者常量中，这个只是用于演示，实际项目中模板代码通常非常多，建议大家按如下方式组织模板代码：\n一个模板的模板代码，保存在一个模板文件中，模板文件名后缀为tpl或tmpl或者其他，如html。编码方式是utf-8。 所有的模板代码都定义在子模板中，方便根据模板名字进行渲染。 所以模板都建议放在templates目录下。使用ParseGlob匹配模式批量解析模板并创建模板对象。 可以将公共的子模板定义在一个文件中common.tpl。 示例：\n模板目录templates， 下面分别按功能模块创建不同的模板文件。\n创建公共模板文件: templates/common.tpl。 主要用于保存一些公共的模板定义：\n1 2 3 4 5 6 7 {{define \u0026#34;common1\u0026#34;}} 这里是共享模块1 {{end}} {{define \u0026#34;common2\u0026#34;}} 这里是共享模块2 {{end}} 创建mod1模块的模板文件: templates/mod1.tpl：\n1 2 3 4 {{define \u0026#34;mod1\u0026#34;}} 这里是模块1 {{- template \u0026#34;common1\u0026#34;}} {{end}} 创建mod2模块的模板文件: templates/mod2.tpl：\n1 2 3 4 {{define \u0026#34;mod2\u0026#34;}} 这里是模块2 {{- template \u0026#34;common2\u0026#34;}} {{end}} 渲染模板代码：\n1 2 3 4 5 6 7 8 //创建template对象，并且加载templates目录下面所有的tpl模板文件。 t := template.Must(template.ParseGlob(\u0026#34;templates/*.tpl\u0026#34;)) // 渲染mod1子模板 t.ExecuteTemplate(os.Stdout, \u0026#34;mod1\u0026#34;, nil) // 渲染mod2子模板 t.ExecuteTemplate(os.Stdout, \u0026#34;mod2\u0026#34;, nil) 运行输出：\n1 2 3 4 5 6 7 8 这里是模块1 这里是共享模块1 这里是模块2 这里是共享模块2 block block语句用于定义一个可重写的模板块。通常用于嵌套模板或布局模板。意思就是：没有重写的模板将使用默认模板渲染。\n格式：\n1 2 3 {{define \u0026#34;base\u0026#34;}} Base template: {{block \u0026#34;content\u0026#34; .}}Default content{{end}} {{end}} 子模板可以重写block：\n1 2 3 4 5 6 7 {{define \u0026#34;content\u0026#34;}} Custom content {{end}} {{define \u0026#34;home\u0026#34;}} {{template \u0026#34;base\u0026#34; .}} {{end}} 注意事项：\n定义可重写的模板块，用block语句，后面跟两个参数：重写的模块名和传递的模板参数（.）。二中缺一不可。 block语句通常与在define语句组合使用，用于在定义模板中定义可重写的模板。 重写模板用define语句。语法格式与定义模板一样。 需要注意的是不能嵌套define语句。 重写模板与定义重写模板不能在同一个模板文件中。 确保define语句在模板文件的顶层定义。 block语句仅在go版本1.19或更高版本以上支持text/template和html/template，以外版本仅支持html/template包或都不支持。 示例：\ntemplates/default/base.tmpl：\n1 2 3 4 {{ define \u0026#34;base\u0026#34; -}} default/index.tmpl: Hello, {{.Name}}! Your message is: {{.Message}} {{ block \u0026#34;custom\u0026#34; . }}custom template {{ .Name }} {{ end }} {{- end }} templates/default/index.tmpl：\n1 2 3 4 5 {{ define \u0026#34;custom\u0026#34; }} ===custom template=== {{ .Message }} {{ end }} {{ define \u0026#34;default/index.tmpl\u0026#34; -}} {{ template \u0026#34;base\u0026#34; .}} {{- end }} main.go：\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 package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;text/template\u0026#34; ) type User struct { Name string Message string } func main() { t, err := template.ParseGlob(\u0026#34;templates/**/*.tmpl\u0026#34;) if err != nil { log.Fatalf(\u0026#34;Fail to parse template: %s\u0026#34;, err) } data := User{Name: \u0026#34;Jack\u0026#34;, Message: \u0026#34;你好\u0026#34;} err = t.ExecuteTemplate(os.Stdout, \u0026#34;default/index.tmpl\u0026#34;, data) if err != nil { log.Fatalf(\u0026#34;Fail to execute template: %s\u0026#34;, err) } } 运行输出：\n1 2 3 $ go run main.go default/index.tmpl: Hello, Jack! Your message is: 你好 ===custom template=== 你好 实现重写模板。\n模板函数介绍 go的模板引擎为我们提供了函数机制，方面我们在处理模板时执行一些特定的功能，例如格式化输出内容、字母大小写转换等等。\n模板函数调用语法 语法格式：\n1 functionName [Argument...] Argument参数是可选的，如果有多个参数，参数直接用空格分隔。\n注意：模板语法都是在{{}}中的，函数调用也是。\n示例：\n1 {{ html \u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;XSS\u0026#39;);\u0026lt;/script\u0026gt;\u0026#34; }} html预定义函数，将html内容进行转义，防止XSS攻击。\n渲染将输出：\n1 \u0026amp;lt;script\u0026amp;gt;alert(\u0026amp;#39;XSS\u0026amp;#39;);\u0026amp;lt;/script\u0026amp;gt; 多个函数参数的示例:\n1 {{ printf \u0026#34;%s: %d\u0026#34; \u0026#34;年龄\u0026#34; 18 }} printf函数主要用于格式化输出字符串，是fmt.Sprintf函数的别名，用法跟fmt.Sprintf函数一样，区别就是模板函数的参数用空格隔开。\n这里为printf函数传递了3个参数。\n渲染将输出：\n1 年龄: 18 预定义模板函数 预定义模板函数也可以叫内置模板函数，是模板引擎预定义好了的，可以直接在模板中拿来使用。下面介绍常用的内置函数：\n前面介绍的比较函数（关系运算函数）也是属于预定义函数。\n也将逻辑运算封装成了函数形式：\n1 2 3 4 5 6 and 表达式1 表达式2 表达式1和表达式2都为真的时候返回true or 表达式1 表达式2 表达式1和表达式2其中一个为真的时候返回true not 表达式 表达式为false则返回true, 反之返回false 官网解释看不明白，差不多就是上面的意思。\n提示: 关系运算和逻辑运算函数通常跟if语句一起使用。\n示例：\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 {{$x := 100}} //等价于$x == 100 {{if eq $x 100}} ...代码... {{end}} //等价于$x \u0026lt; 100 {{if lt $x 500}} ...代码... {{end}} //等价于$x \u0026gt;= 100 {{if ge $x 500}} ...代码... {{end}} //等价于$x \u0026gt; 50 \u0026amp;\u0026amp; $x \u0026lt; 200 //这里调用了and函数和gt、lt三个函数， gt和lt函数的结果作为and的参数，gt和lt函数调用分别用括号包括起来 {{if and (gt $x 50) (lt $x 200)}} ...代码... {{end}} {{$y := 200}} //等价于$x \u0026gt; 100 || $y \u0026gt; 100 {{if or (gt $x 100) (gt $y 100)}} ...代码... {{end}} 更多内置函数：\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 45 len 返回它的参数的整数类型长度，可以计算数组长度。 数组大小: {{len .}} //模板参数定义如下： a := []int{1,2,3,4} 渲染输出: 数组大小: 4 index 返回其第一个参数（通常为数组、切片或映射）的第 N 个元素，N 由后续参数指定。 如\u0026#34;index x 1 2 3\u0026#34;返回x[1][2][3]的值；每个被索引的主体必须是数组、切片或者字典。 data := []string{\u0026#34;first\u0026#34;, \u0026#34;second\u0026#34;, \u0026#34;third\u0026#34;} tmpl := `Second element: {{ index . 1 }}` 渲染输出：Second element: second print 即fmt.Sprint printf 即fmt.Sprintf println 即fmt.Sprintln 主要用于格式化字符串,是go fmt.Sprintf函数的别名，前面的例子已经介绍。 html 返回与其参数的文本表示形式等效的转义HTML。 将其参数作为安全的 HTML 输出。 这个函数在html/template中不可用。 urlquery 以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。 将其参数编码为 URL 查询参数。 这个函数在html/template中不可用。 主要用于url编码。 /search?keyword={{urlquery \u0026#34;搜索关键词\u0026#34;}} /search?keyword=%E6%90%9C%E7%B4%A2%E5%85%B3%E9%94%AE%E8%AF%8D js 返回与其参数的文本表示形式等效的转义JavaScript。 将其参数作为安全的 JavaScript 输出。 {{ js \u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;Hello, World!\u0026#39;);\u0026lt;/script\u0026gt;\u0026#34; }} 渲染输出： \\u003Cscript\\u003Ealert(\\\u0026#39;Hello, World!\\\u0026#39;);\\u003C/script\\u003E call 调用其第一个参数指定的函数，其余参数作为函数参数传递。 执行结果是调用第一个参数的返回值，该参数必须是函数类型，其余参数作为调用该函数的参数； 如\u0026#34;call .X.Y 1 2\u0026#34;等价于go语言里的dot.X.Y(1, 2)； 其中Y是函数类型的字段或者字典的值，或者其他类似情况； call的第一个参数的执行结果必须是函数类型的值（和预定义函数如print明显不同）； 该函数类型值必须有1到2个返回值，如果有2个则后一个必须是error接口类型； 如果有2个返回值的方法返回的error非nil，模板执行会中断并返回给调用模板执行者该错误； call示例(可以先看自定义模板函数部分）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;text/template\u0026#34; ) func sayHello(name string) string { return fmt.Sprintf(\u0026#34;Hello, %s!\u0026#34;, name) } func main() { tmpl := `{{ call . \u0026#34;Gopher\u0026#34; }}` t := template.Must(template.New(\u0026#34;example\u0026#34;).Funcs(template.FuncMap{ \u0026#34;sayHello\u0026#34;: sayHello, }).Parse(tmpl)) t.Execute(os.Stdout, sayHello) } 运行输出：\n1 Hello, Gopher! 管道(pipeline) pipeline 翻译过来可以称为管道或者流水线， pipeline运算的作用是将多个函数调用或者值串起来，从左往右执行，左边执行的结果会传递给右边，形成一个任务流水。\npipeline运算符：| (竖线)\n语法格式:\n1 command1 | command2 | command3 ... command可以是一个值，也可以是一个函数。\n示例1：\n1 {{ \u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;XSS\u0026#39;);\u0026lt;/script\u0026gt;\u0026#34; | html }} 这里意思就是将第一个字符串值传递给html函数。\n渲染将输出：\n1 \u0026amp;lt;script\u0026amp;gt;alert(\u0026amp;#39;XSS\u0026amp;#39;);\u0026amp;lt;/script\u0026amp;gt; 示例2：\n1 {{ \u0026#34;关键词\u0026#34; | html | urlquery }} 这个例子就是先将 \u0026ldquo;关键词\u0026rdquo; 传递给html函数转义下html标签，然后在将html执行结果传递给urlquery函数进行url编码。\n渲染将输出：\n1 %E5%85%B3%E9%94%AE%E8%AF%8D 注意：如果函数有多个参数，pipeline运算会将值传递给函数的最后一个参数, 例如: {{ 100 | printf \u0026quot;value=%d\u0026quot; }}, 这里将100传递给printf函数的最后一个参数。\n自定义模板函数 内置的模板函数使用有限，我们可以自己定义模板函数。\n步骤：\n创建自定义函数。\n将自定义函数映射到模板引擎中。用FuncMap函数映射。本质是map类型。可以映射多个。\n键值都是自定义的函数名。 最后调用Funcs方法，将映射添加到模板中。\n注意请在解析前完成以上操作。\n1 2 3 4 5 6 7 8 9 // 创建模板并添加自定义函数 t := template.New(\u0026#34;example\u0026#34;).Funcs(funcMap) // 解析匹配指定模式的模板文件 t = template.Must(t.ParseGlob(\u0026#34;templates/*.tmpl\u0026#34;)) or // 创建模板并解析 t := template.Must(template.New(\u0026#34;example\u0026#34;).Funcs(funcMap).Parse(tmpl)) 示例：\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 45 package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;text/template\u0026#34; ) func ToUpper(s string) string { return strings.ToUpper(s) } func Repeat(word string, count int) string { return strings.Repeat(word, count) } type Data struct { Message string Word string Count int } func main() { funcMap := template.FuncMap{ \u0026#34;ToUpper\u0026#34;: ToUpper, \u0026#34;Repeat\u0026#34;: Repeat, } const tmpl = `{{ ToUpper .Message }} {{ Repeat .Word .Count }}` data := Data{ Message: \u0026#34;hello, world!\u0026#34;, Word: \u0026#34;Go\u0026#34;, Count: 3, } t := template.Must(template.New(\u0026#34;example\u0026#34;).Funcs(funcMap).Parse(tmpl)) err := t.Execute(os.Stdout, data) if err != nil { log.Fatalf(\u0026#34;Fail to execute template: %s\u0026#34;, err) } } 运行输出：\n1 2 3 $ go run main.go HELLO, WORLD! GoGoGo 修改默认的标识符 Go标准库的模板引擎使用的花括号{{和}}作为标识，而许多前端框架（如Vue和 AngularJS）也使用{{和}}作为标识符，所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突，这个时候我们需要修改标识符，修改前端的或者修改Go语言的。这里演示如何修改Go语言模板引擎默认的标识符：\n1 2 3 const tmpl = `{[ printf \u0026#34;==%s==\u0026#34; . ]}` t := template.Must(template.New(\u0026#34;example\u0026#34;).Delims(\u0026#34;{[\u0026#34;, \u0026#34;]}\u0026#34;).Parse(tmpl)) 用到的方法是Delims，分别接收两端的分隔符为参数。\n应用 在Go语言中，text/template和html/template包用于生成文本和HTML输出，常见的应用场景如下：\ntext/template包：\n生成配置文件：\n通过模板生成动态配置文件，如YAML、JSON等格式，用于不同环境的配置管理。\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 package main import ( \u0026#34;os\u0026#34; \u0026#34;text/template\u0026#34; ) func main() { tmpl, err := template.New(\u0026#34;config\u0026#34;).Parse(` apiVersion: v1 kind: ConfigMap metadata: name: {{.Name}} data: key: {{.Value}} `) if err != nil { panic(err) } data := struct { Name string Value string }{ Name: \u0026#34;example-config\u0026#34;, Value: \u0026#34;example-value\u0026#34;, } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } } 生成代码：\n自动生成代码文件，如生成CRUD代码、接口实现代码等。\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 45 46 47 48 49 package main import ( \u0026#34;os\u0026#34; \u0026#34;text/template\u0026#34; ) func main() { tmpl, err := template.New(\u0026#34;crud\u0026#34;).Parse(` package main type {{.Name}} struct { ID int Name string } func ({{.Receiver}} *{{.Name}}) Create() { // Create logic } func ({{.Receiver}} *{{.Name}}) Read() { // Read logic } func ({{.Receiver}} *{{.Name}}) Update() { // Update logic } func ({{.Receiver}} *{{.Name}}) Delete() { // Delete logic } `) if err != nil { panic(err) } data := struct { Name string Receiver string }{ Name: \u0026#34;User\u0026#34;, Receiver: \u0026#34;u\u0026#34;, } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } } 生成文档：\n生成报告、邮件、日志等文本文件。\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 package main import ( \u0026#34;os\u0026#34; \u0026#34;text/template\u0026#34; ) func main() { tmpl, err := template.New(\u0026#34;report\u0026#34;).Parse(` Report: Name: {{.Name}} Date: {{.Date}} Summary: {{.Summary}} `) if err != nil { panic(err) } data := struct { Name string Date string Summary string }{ Name: \u0026#34;John Doe\u0026#34;, Date: \u0026#34;2024-06-10\u0026#34;, Summary: \u0026#34;This is a summary of the report.\u0026#34;, } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } } html/template包：\nhtml/template包专门用于生成安全的HTML内容，防止XSS（跨站脚本攻击）。以下是一些常见应用：\n生成动态网页：\n根据用户输入或数据库内容动态生成HTML页面，适用于Web应用的前端展示。\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 package main import ( \u0026#34;html/template\u0026#34; \u0026#34;net/http\u0026#34; ) type User struct { Name string Email string } func handler(w http.ResponseWriter, r *http.Request) { tmpl, err := template.New(\u0026#34;user\u0026#34;).Parse(` \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;User Page\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Hello, {{.Name}}\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Email: {{.Email}}\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; `) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } user := User{Name: \u0026#34;Alice\u0026#34;, Email: \u0026#34;alice@example.com\u0026#34;} err = tmpl.Execute(w, user) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func main() { http.HandleFunc(\u0026#34;/\u0026#34;, handler) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 生成邮件内容：\n生成包含HTML格式的邮件内容，用于发送动态邮件。\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 package main import ( \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;html/template\u0026#34; \u0026#34;net/smtp\u0026#34; ) func main() { tmpl, err := template.New(\u0026#34;email\u0026#34;).Parse(` \u0026lt;html\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Hello, {{.Name}}\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Thank you for joining our service.\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; `) if err != nil { panic(err) } var body bytes.Buffer data := struct { Name string }{Name: \u0026#34;Alice\u0026#34;} err = tmpl.Execute(\u0026amp;body, data) if err != nil { panic(err) } auth := smtp.PlainAuth(\u0026#34;\u0026#34;, \u0026#34;your-email@example.com\u0026#34;, \u0026#34;your-email-password\u0026#34;, \u0026#34;smtp.example.com\u0026#34;) err = smtp.SendMail(\u0026#34;smtp.example.com:587\u0026#34;, auth, \u0026#34;your-email@example.com\u0026#34;, []string{\u0026#34;recipient@example.com\u0026#34;}, body.Bytes()) if err != nil { panic(err) } fmt.Println(\u0026#34;Email sent successfully!\u0026#34;) } 生成HTML报告：\n根据模板生成包含动态内容的HTML报告，适用于生成分析结果、统计数据展示等。\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 package main import ( \u0026#34;html/template\u0026#34; \u0026#34;os\u0026#34; ) func main() { tmpl, err := template.New(\u0026#34;report\u0026#34;).Parse(` \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Report\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Report Summary\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Name: {{.Name}}\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;Date: {{.Date}}\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;Summary: {{.Summary}}\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; `) if err != nil { panic(err) } data := struct { Name string Date string Summary string }{ Name: \u0026#34;John Doe\u0026#34;, Date: \u0026#34;2024-06-10\u0026#34;, Summary: \u0026#34;This is a summary of the report.\u0026#34;, } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } } 总结 text/template主要用于生成纯文本内容，如配置文件、代码、文档等。 html/template主要用于生成安全的HTML内容，如动态网页、邮件内容、HTML报告等。 这两个包都通过模板提供了强大的文本处理功能，可以根据需要选择适用的包来生成所需的输出。\n补充 不转义HTML内容 如果你需要安全地显示用户输入的 HTML 内容，可以使用以下几种方法：\n明确信任的内容：\n当我们使用html/template包渲染时，会自动转义HTML内容，如果不希望转义HTML内容，可以将要渲染的HTML内容定义为template.HTML类型（本质就是字符串），这样就不会自动转义HTML内容。\n需要注意的是：对于完全可信的内容，才能使用 template.HTML，以防止XSS攻击。\n对应的信任JavaScript类型template.JS ：不会自动转义为安全的js代码。\n示例：\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 package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; //\u0026#34;text/template\u0026#34; \u0026#34;html/template\u0026#34; ) func main() { const tmpl = \u0026#34;Hello, {{.Name}}! Your message is: {{.Message}}\u0026#34; data := struct { Name string Message template.HTML }{ Name: \u0026#34;World\u0026#34;, Message: \u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;XSS\u0026#39;);\u0026lt;/script\u0026gt;\u0026#34;, } t := template.New(\u0026#34;example\u0026#34;) t, err := t.Parse(tmpl) if err != nil { log.Fatalf(\u0026#34;Parsing template: %s\u0026#34;, err) } err = t.Execute(os.Stdout, data) if err != nil { log.Fatalf(\u0026#34;Excuting templete: %s\u0026#34;, err) } } 运行输出：\n1 2 $ go run main.go Hello, World! Your message is: \u0026lt;script\u0026gt;alert(\u0026#39;XSS\u0026#39;);\u0026lt;/script\u0026gt; 使用 HTML 白名单：\n如果用户提供的内容需要部分 HTML 标签，可以使用 HTML 解析库将用户输入的内容过滤，只允许特定的标签和属性通过。这可以使用第三方库如 bluemonday 来实现。\n示例：\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 package main import ( \u0026#34;html/template\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/microcosm-cc/bluemonday\u0026#34; ) func sanitizeHTML(input string) template.HTML { p := bluemonday.UGCPolicy() // 使用默认的用户生成内容策略 sanitized := p.Sanitize(input) return template.HTML(sanitized) } func main() { const tmpl = `Hello, {{.Name}}! Your message is: {{.Message}}` data := struct { Name string Message template.HTML }{ Name: \u0026#34;World\u0026#34;, Message: sanitizeHTML(\u0026#34;\u0026lt;strong\u0026gt;Bold Text\u0026lt;/strong\u0026gt;\u0026#34; + \u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;XSS\u0026#39;);\u0026lt;/script\u0026gt;\u0026#34;), } t, err := template.New(\u0026#34;example\u0026#34;).Parse(tmpl) if err != nil { log.Fatalf(\u0026#34;parsing template: %s\u0026#34;, err) } err = t.Execute(os.Stdout, data) if err != nil { log.Fatalf(\u0026#34;executing template: %s\u0026#34;, err) } } 运行输出：\n1 2 $ go run main.go Hello, World! Your message is: \u0026lt;strong\u0026gt;Bold Text\u0026lt;/strong\u0026gt; 使用 Markdown：\n另一种方式是使用 Markdown，将用户输入的 Markdown 转换为安全的 HTML。这种方式适用于允许用户使用简单标记语言格式化内容的场景。用到的第三方库是blackfriday。\n示例：\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 package main import ( \u0026#34;html/template\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/russross/blackfriday/v2\u0026#34; ) func markdownToHTML(input string) template.HTML { output := blackfriday.Run([]byte(input)) return template.HTML(output) } func main() { const tmpl = `Hello, {{.Name}}! Your message is: {{.Message}}` data := struct { Name string Message template.HTML }{ Name: \u0026#34;World\u0026#34;, Message: markdownToHTML(\u0026#34;**Bold Text**\\n\\n\u0026#34; + \u0026#34;\u0026lt;script\u0026gt;alert(\u0026#39;XSS\u0026#39;);\u0026lt;/script\u0026gt;\u0026#34;), } t, err := template.New(\u0026#34;example\u0026#34;).Parse(tmpl) if err != nil { log.Fatalf(\u0026#34;parsing template: %s\u0026#34;, err) } err = t.Execute(os.Stdout, data) if err != nil { log.Fatalf(\u0026#34;executing template: %s\u0026#34;, err) } } 运行输出：\n1 2 3 4 $ go run main.go Hello, World! Your message is: \u0026lt;p\u0026gt;\u0026lt;strong\u0026gt;Bold Text\u0026lt;/strong\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;\u0026lt;script\u0026gt;alert(\u0026amp;lsquo;XSS\u0026amp;rsquo;);\u0026lt;/script\u0026gt;\u0026lt;/p\u0026gt; 在这个例子中，任何脚本标签或其他潜在的 XSS 攻击内容都会被转义，确保生成的 HTML 是安全的。\n总结\n明确信任的内容：对于完全可信的内容，使用 template.HTML。 使用 HTML 白名单：使用 HTML 解析库，如 bluemonday，过滤用户输入，只允许特定的标签和属性。 使用 Markdown：将用户输入的 Markdown 转换为安全的 HTML，避免直接嵌入用户提供的 HTML。 这几种方法可以在保证安全性的前提下，允许一定程度的 HTML 内容显示。选择具体方法时，应根据应用场景和安全需求做出适当选择。\n参考 Golang模板引擎快速入门教程 Go语言标准库之http/template ","date":"2024-05-26T14:00:53+08:00","permalink":"https://arlettebrook.github.io/p/template-introduction/","title":"Template Introduction"},{"content":" 作为一名开发者，往往需要编写程序的 API 文档，尤其是 Web 后端开发者，在跟前端对接 HTTP 接口的时候，一个好的 API 文档能够大大提高协作效率，降低沟通成本，本文就来聊聊如何使用 OpenAPI 构建 HTTP 接口文档。\nOpenAPI 什么是 OpenAPI OpenAPI 是规范化描述 API 领域应用最广泛的行业标准，由 OpenAPI Initiative(OAI) 定义并维护，同时也是 Linux 基金会下的一个开源项目。通常我们所说的 OpenAPI 全称应该是 OpenAPI Specification(OpenAPI 规范，简称 OSA)，它使用规定的格式来描述 HTTP RESTful API 的定义，以此来规范 RESTful 服务开发过程。使用 JSON 或 YAML 来描述一个标准的、与编程语言无关的 HTTP API 接口。OpenAPI 规范最初基于 SmartBear Software 在 2015 年捐赠的 Swagger 规范演变而来，目前最新的版本是 v3.1.0。\n简单来说，OpenAPI 就是用来定义 HTTP 接口文档的一种规范，大家都按照同一套规范来编写接口文档，能够极大的减少沟通成本。\nOpenAPI 规范基本信息 OpenAPI 规范内容包含非常多的细节，本文无法一一讲解，这里仅介绍常见的基本信息，以 YAML 为例进行说明。YAML 是 JSON 的超集，在 OpenAPI 规范中定义的所有语法，两者之间是可以互相转换的，如果手动编写，建议编写 YAML 格式，更为易读。\nOpenAPI 文档编写在一个 .json 或 .yaml 中，推荐将其命名为 openapi.json 或 openapi.yaml，OpenAPI 文档其实就是一个单一的 JSON 对象，其中包含符合 OpenAPI 规范中定义的结构字段。\nOpenAPI 规范基本信息如下：\n字段名 类型 描述 openapi string 必选，必须是 OpenAPI 已发布的合法版本，如 3.0.1。 info object 必选，此字段提供 API 相关的元数据（如描述、作者和联系信息）。 servers array[object] 这是一个 Server 对象的数组，提供到服务器的连接信息。 paths object 必选，API 提供的可用的路径和操作。 components object 一个包含多种结构的元素，可复用组件。 security array 声明 API 使用的安全认证机制，目前支持 HTTP Basic Auth、HTTP Bearer Auth、ApiKey Auth 以及 OAuth2。 tags array 提供标签可以为 API 归类，每个标签名都应该是唯一的。 externalDocs object 附加的文档，可以通过扩展属性来扩展文档。 一个 YAML 格式的 OpenAPI 文档示例如下：\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 45 46 47 48 49 50 51 52 openapi: 3.1.0 info: title: Tic Tac Toe description: | This API allows writing down marks on a Tic Tac Toe board and requesting the state of the board or of individual squares. version: 1.0.0 # 此为 API 接口文档版本，与 openapi 版本无关 tags: - name: Gameplay paths: # Whole board operations /board: get: summary: Get the whole board description: Retrieves the current state of the board and the winner. tags: - Gameplay operationId: get-board responses: \u0026#34;200\u0026#34;: description: \u0026#34;OK\u0026#34; content: application/json: schema: $ref: \u0026#34;#/components/schemas/status\u0026#34; # Single square operations /board/{row}/{column}: parameters: - $ref: \u0026#34;#/components/parameters/rowParam\u0026#34; - $ref: \u0026#34;#/components/parameters/columnParam\u0026#34; get: summary: Get a single board square description: Retrieves the requested square. tags: - Gameplay operationId: get-square responses: \u0026#34;200\u0026#34;: description: \u0026#34;OK\u0026#34; content: application/json: schema: $ref: \u0026#34;#/components/schemas/mark\u0026#34; \u0026#34;400\u0026#34;: description: The provided parameters are incorrect content: text/html: schema: $ref: \u0026#34;#/components/schemas/errorMessage\u0026#34; example: \u0026#34;Illegal coordinates\u0026#34; ... 以上示例完整文档在此，具体语法我就不在这里介绍了。如果你开发过 API 接口，相信能看懂文档大部分内容所代表的含义。不必完全掌握其语法，这并不会对阅读本文接下来的内容造成困扰，因为稍后我会介绍如何通过代码注释的方式自动生成此文档。\n如果你想手动编写 OpenAPI 文档，那么我还是推荐你阅读下 OpenAPI 规范，这里有一份中文版的规范。阅读规范是一个比较枯燥的过程，如果你没有耐心读完，强烈建议阅读 OpenAPI 规范入门，相较于完整版的规范要精简得多，并且讲解更加易于理解。\n另外还推荐访问 OpenAPI Map 网站来掌握 OpenAPI 规范，该网站以思维导图的形式展现规范的格式以及说明。\nOpenAPI.Tools 现在我们知道了 OpenAPI 规范，接下来要做什么？当然是了解 OpenAPI 开放了哪些能力。\n有一个叫 OpenAPI.Tools 的网站，分类整理并记录了社区围绕 OpenAPI 规范开发的流行工具。\n可以看到列表中有很多分类，在我们日常开发中，最经常使用的有三类：\n文档编辑器 文档编辑器方便我们用来编写符合 OpenAPI 规范的文档，有助于提高编写文档的效率，就像 VS Code 能够方便我们编写代码一样。\n文档编辑器有两种：文本编辑器 以及 图形编辑器。\n文本编辑器推荐使用在线的 Swagger Editor，能够实现格式校验和实时预览 Swagger 交互式 API 文档功能，效果如下图所示：\n如果你习惯使用 VS Code，也有相应插件可供使用。\n图形编辑器的好处是能够以可视化的形式编辑内容，不了解 OpenAPI 规范语法也能编辑。可以根据自己喜好来进行选择，如 Stoplight Studio、APIGit 等。\nMock 服务器 当我们使用 OpenAPI 规范来进行接口开发时，往往采用文档先行的策略，也就是前后端在开发代码前，先定义好接口文档，再进行代码的编写。此时前端如果想测试接口可用性，而后端代码还没有编写完成，Mock 服务器就派上用场了。Mock 服务器能够根据所提供的 OpenAPI 接口文档，自动生成一个模拟的 Web Server。使用 Mock 服务器能够轻松模拟真实的后端接口，方便前端同学进行接口调试。\n上面提到的 APIGit 也同时具备此功能。\n代码生成器 还有一种很实用的工具是代码生成器，代码生成器有两种类型：一种是从代码/注释生成 OpenAPI 文档，另一种是从 OpenAPI 文档生成代码。\n这类工具同样非常多，且更为实用。比如我们有一份写好了的 Go Web Server 代码，想要自动生成一份 OpenAPI 文档，就可以使用 go-swagger 这个工具来生成一份 openapi.yaml 文档。\n而如果我们有一份 openapi.yaml 文档，就可以利用 go-swagger 生成一份 Go SDK 代码，甚至它还能根据这份 OpenAPI 文档生成 Go Web Server 的框架代码，我们只需要在对应的接口里面实现具体的业务逻辑即可。\n不仅 Go 语言有这样的工具，像 Swagger Codegen 和 OpenAPI Generator 这类工具更是支持几乎所有主流编程语言。\n代码生成器是开发者应该着重关注的工具，使用这些工具可以减少大量手动且重复的工作，你可以在此看下有没有感兴趣的项目供你使用。\nSwagger 什么是 Swagger Swagger 是一套围绕 OpenAPI 规范所构建的开源工具集，提供了强大和易于使用的工具来充分利用 OpenAPI 规范，Swagger 工具集由最初的 Swagger 规范背后的团队所开发。\nSwagger 工具集提供了 API 设计、开发、测试、治理和监控等能力，其中最主要的工具包含如下三个：\nSwagger Codegen：根据 OpenAPI 规范定义生成服务器存根和客户端 SDK。 Swagger Editor：基于浏览器的在线 OpenAPI 规范编辑器。 Swagger UI：以 UI 界面的方式可视化展示 OpenAPI 规范定义，并且能够在浏览器中进行交互。 当然 Swagger 也有为企业用户提供的收费版本工具，如 SwaggerHub Enterprise，感兴趣的同学可以自行了解。\nSwagger 和 OpenAPI 的关系 讲到了 Swagger，就不得不提及 Swagger 和 OpenAPI 的联系与区别，因为这二者经常在一起出现。\n前文也说过 OpenAPI 规范是基于 Swagger 规范演变而来的，但其实二者并不相等。\n在 OpenAPI 尚未出现之前，Swagger 代表了 Swagger 规范以及一系列围绕 Swagger 规范的开源工具集。Swagger 规范最后一个版本是 2.0，之后就捐赠给了 OAI 并被重新命名为 OpenAPI 规范，所以 OpenAPI 规范第一个版本是 2.0，也就是 Swagger 规范 2.0，而由 OAI 这个组织发布的第一个 OpenAPI 规范正式版本是 3.0.0。\n现在，Swagger 规范已被 OpenAPI 规范完全接管并取代。OpenAPI 代表了 OpenAPI 规范以及一系列生态，而 Swagger 则是这个生态中的一部分，是 Swagger 团队围绕 OpenAPI 规范所开发的一系列工具集。\nSwagger 是 OpenAPI 生态中非常重要的组成部分，因为它给出了一整套方案，且非常流行。\nSwagger 和 OpenAPI 二者 LOGO 对比如下：\n希望你下次再见到这两个 LOGO 时能清晰分辨出二者，而不被混淆。\n以 Go 语言为例讲解 OpenAPI 在实际开发中的应用 前文介绍了编写 OpenAPI 文档的两种编辑器：文本编辑器以及图形编辑器。在日常开发中，后端可以先使用这类编辑器如 Swagger Editor 编写出 OpenAPI 文档，然后将这份文档交给前端，前端拿到 OpenAPI 文档后将其导入到 Swagger Editor，就可以在线阅读接口文档并与之进行交互，之后前后端就可以并行开发了。\n这样的工作流看起来似乎没什么问题，不过编写 OpenAPI 文档毕竟是个苦力活，不仅有大量的重复工作，还要求开发者熟练掌握 OpenAPI 规范语法。这对于“爱偷懒”的开发者显然是无法接受的，就像段子里说的，程序员最讨厌两件事：1. 写文档，2. 别人不写文档。而这个问题的解法，当然就是前文提到的代码生成器。\n使用 Swag 生成 Swagger 文档 在 Go 语言生态里，目前有两个比较流行的开源工具可以生成 Swagger 文档，分别是 go-swagger 和 swag。它们都能根据代码中的注释生成 Swagger 文档，go-swagger 作为一款 OpenAPI.Tools 推荐的工具，其功能比 swag 更加强大且 Github Star 数量也更高。\n不过本文将选择 swag 来进行介绍，一是因为 swag 比较轻量，更适合微服务开发；二是如果使用 swag，那么注释代码会离接口代码更近，升级时方便维护。如果你有更高级的需求，如根据 Swagger 文档生成客户端 SDK，服务端存根等，则推荐使用 go-swagger。\n注意：在这里我一直提到的都是生成 Swagger 文档，而没有说是 OpenAPI 文档。因为无论是 swag 还是功能更强大的 go-swagger，它们目前都仅支持生成 OpenAPI 2.0 文档，并不支持生成 OpenAPI 3.0+ 文档，而 OpenAPI 2.0 版本我们更习惯称其为 Swagger 文档。\n安装 Swag 1 2 3 $ go install github.com/swaggo/swag/cmd/swag@latest # 安装 $ swag --version # 查看版本 swag version v1.8.10 Swag 命令行工具 swag 非常简洁，仅提供了两个主要命令 init 和 fmt：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 $ swag -h # 查看帮助 NAME: swag - Automatically generate RESTful API documentation with Swagger 2.0 for Go. USAGE: swag [global options] command [command options] [arguments...] VERSION: v1.8.10 COMMANDS: init, i Create docs.go fmt, f format swag comments help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false) --version, -v print the version (default: false) 在包含 main.go 文件（默认情况下）的项目根目录运行 swag init 命令，将会解析 swag 注释并生成 docs/ 目录以及 /docs/docs.go、docs/swagger.json、docs/swagger.yaml 三个文件。\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 $ swag init -h # 查看 init 子命令使用方法 NAME: swag init - Create docs.go USAGE: swag init [command options] [arguments...] OPTIONS: --quiet, -q 不在控制台输出日志 (default: false) --generalInfo value, -g value API 通用信息所在的 Go 源文件路径，如果是相对路径则基于 API 解析目录 (default: \u0026#34;main.go\u0026#34;) --dir value, -d value API 解析目录，多个目录可用逗号分隔 (default: \u0026#34;./\u0026#34;) --exclude value 解析扫描时排除的目录，多个目录可用逗号分隔 --propertyStrategy value, -p value 结构体字段命名规则，三种：snake_case，camelCase，PascalCase (default: \u0026#34;camelCase\u0026#34;) --output value, -o value 所有生成文件的输出目录（swagger.json, swagger.yaml and docs.go）(default:\u0026#34;./docs\u0026#34;) --outputTypes value, --ot value 生成文件的输出类型（docs.go, swagger.json, swagger.yaml）三种：go,json,yaml (default: \u0026#34;go,json,yaml\u0026#34;) --parseDependency, --pd 解析依赖目录中的 Go 文件 (default: false) --markdownFiles value, --md value 指定 API 的描述信息所使用的 Markdown 文件所在的目录，默认禁用 --parseInternal 解析 internal 包中的 Go 文件 (default: false) --generatedTime 输出时间戳到输出文件 `docs.go` 顶部 (default: false) --parseDepth value 依赖项解析深度 (default: 100) --requiredByDefault 默认情况下，为所有字段设置 `required` 验证 (default: false) --instanceName value 设置文档实例名 (default: \u0026#34;swagger\u0026#34;) --parseGoList 通过 \u0026#39;go list\u0026#39; 解析依赖关系 (default: true) --tags value, -t value 逗号分隔的标签列表，用于过滤指定标签生成 API 文档。特殊情况下，如果标签前缀是 \u0026#39;!\u0026#39; 字符，那么带有该标记的 API 将被排除 --help, -h 显示帮助信息 (default: false) 注意：以上 swag init 命令可选参数介绍略有删减，只列出了常用选项，更完整的文档请参考官方仓库。\nswag fmt 命令可以格式化 swag 注释。\n1 2 3 4 5 6 7 8 9 10 11 12 $ swag fmt -h # 查看 fmt 子命令使用方法 NAME: swag fmt - format swag comments USAGE: swag fmt [command options] [arguments...] OPTIONS: --dir value, -d value API 解析目录，多个目录可用逗号分隔 (default: \u0026#34;./\u0026#34;) --exclude value 解析扫描时排除的目录，多个目录可用逗号分隔 --generalInfo value, -g value API 通用信息所在的 Go 源文件路径，如果是相对路径则基于 API 解析目录 (default: \u0026#34;main.go\u0026#34;) --help, -h 显示帮助信息 (default: false) 在 Gin 中使用 Swag 在 gin 框架能够很方便的使用 swag，步骤如下：\n准备项目目录结构如下：\n1 2 3 4 . ├── go.mod ├── go.sum └── main.go 初始化项目\n1 $ go mod init gin-swag 编写 main.go 代码如下：\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 package main import ( \u0026#34;net/http\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) // Helloworld godoc // @Summary 该操作的简短摘要 // @Description 操作行为的详细说明 // @Tags example // @Accept json // @Produce json // @Success 200 {string} string \u0026#34;Hello World!\u0026#34; // @Router /example/helloworld [get] func Helloworld(g *gin.Context) { g.JSON(http.StatusOK, \u0026#34;Hello World!\u0026#34;) } // @title Swagger Example API // @version 1.0 // @schemes http // @host localhost:8080 // @BasePath /api/v1 // @tag.name example // @tag.description 示例接口 func main() { r := gin.Default() v1 := r.Group(\u0026#34;/api/v1\u0026#34;) { eg := v1.Group(\u0026#34;/example\u0026#34;) { eg.GET(\u0026#34;/helloworld\u0026#34;, Helloworld) } } if err := r.Run(\u0026#34;:8080\u0026#34;); err != nil { panic(err) } } 代码中的注释部分即为 swag 的注释语法，稍后通过这些注释生成 Swagger 文档。\n其中通用 API 信息部分注释含义如下：\n注释 说明 @title 必填，应用程序的名称。 @version 必填，提供应用程序 API 的版本。 @schemes 用空格分隔的请求传输协议。 @host 运行 API 的主机（主机名或 IP 地址）。 @BasePath 运行 API 的基本路径。 @tag.name 标签的名称。 @tag.description 标签的描述。 还有一部分注释代表了 API 操作，其含义如下：\n注释 说明 @Summary 该操作的简短摘要。 @Description 操作行为的详细说明。 @Tags 该 API 操作的标签列表，多个标签以逗号分隔。 @Accept API 可以接收的参数 MIME 类型列表。 @Produce API 可以生成的参数 MIME 类型列表。 @Success 成功响应。 @Router 路由路径定义。 以上这些注释最终都会对应到 OpenAPI 2.0 规范的某个字段上。更多说明请参考官方文档，并且官方也提供了中文文档。\n使用 swag 根据注释生成 Swagger 文档，在项目根目录下（.）执行 swag init，将得到新的目录结构：\n1 2 3 4 5 6 7 8 . ├── docs │ ├── docs.go │ ├── swagger.json │ └── swagger.yaml ├── go.mod ├── go.sum └── main.go 可以发现 swag init 生成的三个文件 docs.go、swagger.json、swagger.yaml 默认都在 docs/ 目录下。\n其中 swagger.json、swagger.yaml 正是符合 OpenAPI 2.0 规范的 JSON 和 YAML 接口文档，例如 swagger.yaml 内容如下：\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 basePath: /api/v1 host: localhost:8080 info: contact: {} title: Swagger Example API version: \u0026#34;1.0\u0026#34; paths: /example/helloworld: get: consumes: - application/json description: 操作行为的详细说明 produces: - application/json responses: \u0026#34;200\u0026#34;: description: Hello World! schema: type: string summary: 该操作的简短摘要 tags: - example schemes: - http swagger: \u0026#34;2.0\u0026#34; tags: - description: 示例接口 name: example 对比上面代码中的注释，很容易将其对应起来，相比于直接编写 YAML 格式文档，显然在代码中编写注释更为简单。\n将其复制到 Swagger Editor 编辑器中即可查看 Swagger UI 预览。或者在GoLand IDE中直接打开，右侧会出现 Swagger UI 预览。\n​\t将 Gin 作为 Swagger UI 服务器 上面我们通过 swag 生成了 Swagger 文档，并手动将生成的 swagger.yaml 复制到 Swagger Editor 编辑器进行 Swagger UI 预览。不过这么做显然有点麻烦，好在 swag 作者也考虑到了这一点，所以他又提供了另外两个项目 gin-swagger 和 files，能够直接将 gin 作为 Swagger UI 服务器，这样就不用每次都将 swagger.yaml 手动复制到 Swagger Editor 编辑器才能实现 Swagger UI 预览。\n使用步骤如下：\n下载 gin-swagger、files:\n1 2 3 $ go get -u github.com/swaggo/swag $ go get -u github.com/swaggo/gin-swagger $ go get -u github.com/swaggo/files 在代码中导入 gin-swagger、files(可以直接注册swagger文档路由，IDE会自动导入）:\n1 2 import ginSwagger \u0026#34;github.com/swaggo/gin-swagger\u0026#34; // gin-swagger middleware import swaggerFiles \u0026#34;github.com/swaggo/files\u0026#34; // swagger embed files 注意：在注册路由时，还需要空导入docs。\n注册 Swagger 文档路由地址:\n1 r.GET(\u0026#34;/swagger/*any\u0026#34;, ginSwagger.WrapHandler(swaggerFiles.Handler)) 完整代码如下：\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 45 46 47 package main import ( \u0026#34;net/http\u0026#34; swaggerFiles \u0026#34;github.com/swaggo/files\u0026#34; ginSwagger \u0026#34;github.com/swaggo/gin-swagger\u0026#34; _ \u0026#34;github.com/arlettebrook/learn/docs\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) // Helloworld godoc // @Summary 该操作的简短摘要 // @Description 操作行为的详细说明 // @Tags example // @Accept json // @Produce json // @Success 200 {string} string \u0026#34;Hello World!\u0026#34; // @Router /example/helloworld [get] func Helloworld(g *gin.Context) { g.JSON(http.StatusOK, \u0026#34;Hello World!\u0026#34;) } // @title Swagger Example API // @version 1.0 // @schemes http // @host localhost:8080 // @BasePath /api/v1 // @tag.name example // @tag.description 示例接口 func main() { r := gin.Default() v1 := r.Group(\u0026#34;/api/v1\u0026#34;) { eg := v1.Group(\u0026#34;/example\u0026#34;) { eg.GET(\u0026#34;/helloworld\u0026#34;, Helloworld) } } r.GET(\u0026#34;/swagger/*any\u0026#34;, ginSwagger.WrapHandler(swaggerFiles.Handler)) if err := r.Run(\u0026#34;:8080\u0026#34;); err != nil { panic(err) } } 执行 go run main.go 启动服务，访问 http://localhost:8080/swagger/index.html 即可查看 Swagger UI 交互式文档界面。\n​\n这个本地的 Swagger UI 服务器同样支持交互式操作。\n展开 /example/helloworld 这个接口，点击 Try it out。\n接着，点击 Execute。\nSwagger UI 将会根据文档的 Base URL 去请求真正的接口（同时还会给出 cURL 发送请求的命令，方便复制使用），并将响应结果展示出来。\n同时后端服务器能够打印出请求记录：\n与前端对接时，我们只需要将接口文档地址给到前端，前端就可以根据这个 Swagger UI 界面进行接口查阅和调试了，非常方便。\n让 Swag 支持多版本 API 文档 实际工作中，我们的项目会比这个只有一个接口的 demo 复杂得多，同时 API 也可能会支持多版本，比如 /api/v1、/api/v2。\n我们可以分别生成 v1、v2 两个版本的 API 文档，这样可以将不同版本的接口分开展示，更加清晰。\n命令如下：\n1 2 swag init -g internal/api/controller/v1/docs.go --exclude internal/api/controller/v2 --instanceName v1 swag init -g internal/api/controller/v2/docs.go --exclude internal/api/controller/v1 --instanceName v2 其中 -g 参数指明 API 通用注释信息所在的 Go 源文件路径，大型项目中为了保持代码架构整洁，这些注释应该独立于一个文件docs.go，而不是直接写在 main.go 中。\n--exclude 参数指明生成 Swagger 文档时，需要排除的目录。可以发现，在生成 v1 版本接口文档时，我排除了 v2 接口目录，在生成 v2 版本接口文档时，排除了 v1 接口目录，这样就实现了多版本接口分离。\n别忘了注册路由：\n1 2 3 r.GET(\u0026#34;/swagger/v1/*any\u0026#34;, ginSwagger.WrapHandler(swaggerFiles.NewHandler(), ginSwagger.InstanceName(\u0026#34;v1\u0026#34;))) r.GET(\u0026#34;/swagger/v1/*any\u0026#34;, ginSwagger.WrapHandler(swaggerFiles.NewHandler(), ginSwagger.InstanceName(\u0026#34;v1\u0026#34;))) 技巧：每次修改swagger注释，都需要重新运行swag init命令，并且这里的命令还很复杂：\n我们可以使用make命令，帮我们简化构建swagger文档命令。Makefile文件内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 .PHONY: run run: swag-fmt swag-init go run main.go .PHONY: swag-fmt swag-fmt: swag fmt .PHONY: swag-init swag-init: swag-fmt swag init -g internal/api/controller/v1/docs.go --exclude internal/api/controller/v2 --instanceName v1 swag init -g internal/api/controller/v2/docs.go --exclude internal/api/controller/v1 --instanceName v2 在项目根目录下运行make命令即可自动运行格式化、生成swaggerr文档和启动gin框架。不支持热重载，修改源码之后需要重新运行make命令。\n使用air热重载，自动运行swag构建命令：\n默认情况下air并不支持自动运行swag命令，需要修改默认配置：\n1 $ air init # 生成默认配置文件 修改默认配置文件内容：\n1 2 3 4 5 ... pre_cmd = [\u0026#34;swag fmt\u0026#34;, \u0026#34;swag init -g internal/api/controller/v1/docs.go --exclude internal/api/controller/v2 --instanceName v1\u0026#34;, \u0026#34;swag init -g internal/api/controller/v2/docs.go --exclude internal/api/controller/v1 --instanceName v2\u0026#34;] ... exclude_dir = [\u0026#34;assets\u0026#34;, \u0026#34;tmp\u0026#34;, \u0026#34;vendor\u0026#34;, \u0026#34;testdata\u0026#34;,\u0026#34;docs\u0026#34;] ... 注意：别忘记忽略docs目录，防止热重载死循环。\n之后我们可以直接运行air命令即可自动构建swagger文档以及启动gin框架。并且使用air命令启动之后，每次修改注释和代码，都不需要重新启动程序。air当检测到文件修改时会自动重载，运行构建命令。\n参考：\n廖雪峰Makefile教程 Makefile 简明教程 air基本使用 完整示例代码：swag-example Swag 使用建议 在前文介绍的 swag 使用流程中，不知道你有没有注意到，我们是先编写的代码，然后再生成的 Swagger 文档，最后将这份文档交给前端使用。\n这显然违背了「文档先行」的思想，实际工作中，我们更多的时候是先跟前端约定好接口，然后后端提供 Swagger 文档供前端使用，最后才是前后端编码阶段。\n要想解决这个问题，最直接的解决方案是不使用 swag 工具，而是直接使用 Swagger Editor 这种编辑器手写 Swagger 文档，这样就能实现文档先行了。\n但这又违背了 OpenAPI 给出的「最佳实践」，推荐自动生成 Swagger 文档，而非手动编写。\n我自己的解决方案是，依旧选择使用 swag 工具，不过在编写代码时，先写接口的框架代码，而不写具体的业务逻辑，这样就能够先通过接口注释生成 Swagger 文档，供前端使用，然后再编写业务代码。\n另外，较为遗憾的是，目前（2024-5-23） swag 生成的文档是 OpenAPI 2.0 版本，并不能直接生成 OpenAPI 3.0 版本，如果你想使用 OpenAPI 3.0 版本的文档，一个变通的方法是使用工具将 OpenAPI 2.0 文档转换成 OpenAPI 3.0，如前文提到的 Swagger Editor 就支持此操作。\n使用 ReDoc 风格的 API 文档 也许相较于 Swagger UI 多年不变的界面风格，你更喜欢 ReDoc 风格的 UI，那么 go-redoc 是一个比较不错的选择。\n在 gin 中使用 go-redoc 非常简单，只需要将如下套路代码加入到我们的 main.go 文件中即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;github.com/mvrilo/go-redoc\u0026#34; ginRedoc \u0026#34;github.com/mvrilo/go-redoc/gin\u0026#34; ) ... doc := redoc.Redoc{ Title: \u0026#34;Example API\u0026#34;, Description: \u0026#34;Example API Description\u0026#34;, SpecFile: \u0026#34;./openapi.json\u0026#34;, // \u0026#34;./openapi.yaml\u0026#34; OpenAPI文档路径 SpecPath: \u0026#34;/openapi.json\u0026#34;, // \u0026#34;/openapi.yaml\u0026#34; OpenAPI文档资源路径 DocsPath: \u0026#34;/docs\u0026#34;, // 文档访问路径 } r := gin.New() r.Use(ginRedoc.New(doc)) 还有别忘了添加依赖：\n1 2 $ go get -u github.com/mvrilo/go-redoc $ go get -u github.com/mvrilo/go-redoc/gin 现在完整代码如下：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package main import ( \u0026#34;net/http\u0026#34; \u0026#34;github.com/mvrilo/go-redoc\u0026#34; ginRedoc \u0026#34;github.com/mvrilo/go-redoc/gin\u0026#34; swaggerFiles \u0026#34;github.com/swaggo/files\u0026#34; ginSwagger \u0026#34;github.com/swaggo/gin-swagger\u0026#34; _ \u0026#34;github.com/arlettebrook/learn/docs\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) // Helloworld godoc // //\t@Summary\t该操作的简短摘要 //\t@Description\t操作行为的详细说明 //\t@Tags\texample //\t@Accept\tjson //\t@Produce\tjson //\t@Success\t200\t{string}\tstring\t\u0026#34;Hello World!\u0026#34; //\t@Router\t/example/helloworld [get] func Helloworld(g *gin.Context) { g.JSON(http.StatusOK, \u0026#34;Hello World!\u0026#34;) } // @title\tSwagger Example API // @version\tv1.0 // @schemes\thttp // @host\tlocalhost:8080 // @BasePath\t/api/v1 // @tag.name\texample // @tag.description\t示例接口 func main() { r := gin.Default() doc := redoc.Redoc{ Title: \u0026#34;ReDoc Example API\u0026#34;, Description: \u0026#34;ReDoc Example API Description\u0026#34;, SpecFile: \u0026#34;./docs/swagger.json\u0026#34;, SpecPath: \u0026#34;/swagger.json\u0026#34;, DocsPath: \u0026#34;/redoc\u0026#34;, } r.Use(ginRedoc.New(doc)) v1 := r.Group(\u0026#34;/api/v1\u0026#34;) { eg := v1.Group(\u0026#34;/example\u0026#34;) { eg.GET(\u0026#34;/helloworld\u0026#34;, Helloworld) } } r.GET(\u0026#34;/swagger/v1/*any\u0026#34;, ginSwagger.WrapHandler(swaggerFiles.Handler)) //r.GET(\u0026#34;/swagger/v2/*any\u0026#34;, ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.InstanceName(\u0026#34;v1\u0026#34;))) if err := r.Run(\u0026#34;localhost:8080\u0026#34;); err != nil { panic(err) } } 执行 go run main.go 启动服务，访问http://localhost:8080/redoc即可查看 Redoc UI。\n不过，相较于 Swagger UI，Redoc UI 有个弊端是不能实现交互式操作，如果仅以此作为文档查阅工具，没有交互式操作的需求，那么还是比较推荐使用的。\n更先进的 API 工具 除了 OpenAPI.Tools 推荐的开源工具，社区中其实还有很多其他优秀工具值得尝试使用，比如我这里要推荐的一款国产工具 Apifox，官方将其定义为 Apifox = Postman + Swagger + Mock + JMeter，集 API 设计/开发/测试 于一身。\nApifox 可谓一站式图形化工具，其功能非常强大，就像前文提到的 APIGit 同时具备了编辑器和 Mock 服务器的功能，Apifox 有过之而无不及。\n图形化工具上手难度不大，加上 Apifox 本身由国内开发，非常容易上手，所以本文也就不深入介绍了，你可以观看官方教程 21 分钟学会 Apifox 来学习使用。\nApipost也是跟它差不多的接口测试工具，不过我更喜欢apipost，因为它不登录的情况下，离线可以使用。\n参考 OpenAPI 官网： https://www.openapis.org/ OpenAPI 入门： https://oai.github.io/Documentation/ OpenAPI 规范： https://spec.openapis.org/oas/latest.html OpenAPI 规范中文版： https://openapi.apifox.cn/ OpenAPI 规范思维导图版： https://openapi-map.apihandyman.io/ OpenAPI.Tools： https://openapi.tools/ Swagger 官网： https://swagger.io/ swag： https://github.com/swaggo/swag swag-example： https://github.com/jianghushinian/swag-example go-redoc： https://github.com/mvrilo/go-redoc 原文：使用 OpenAPI 构建 API 文档 ","date":"2024-05-22T21:43:36+08:00","permalink":"https://arlettebrook.github.io/p/openapi-and-swagger-introduction/","title":"Openapi And Swagger Introduction"},{"content":" Gin介绍 Gin 是一个用 Golang编写的高性能的web 框架, 由于http路由的优化，运行速度非常快，速度提高了近 40 倍。 Gin的特点就是封装优雅、API友好。\nGin 最擅长的就是API接口的高并发，如果项目的规模不大，业务相对简单，这个时候我们也推荐您使用 Gin。\n当某个接口的性能遭到较大挑战的时候，这个还是可以考虑使用 Gin 重写接口。\nGin 也是一个流行的 golang Web 框架，Github Strat 量已经超过了 75k[2024/05/21]。\nGin的一些特性：\n快速 基于 Radix 树的路由，小内存占用。没有反射。可预测的 API 性能。 支持中间件 传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如：Logger，Authorization，GZIP，最终操作 DB。 Crash 处理 Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样，你的服务器将始终可用。例如，你可以向 Sentry 报告这个 panic！ JSON 验证 Gin 可以解析并验证请求的 JSON，例如检查所需值的存在。 路由组 更好地组织路由。是否需要授权，不同的 API 版本…… 此外，这些组可以无限制地嵌套而不会降低性能。 错误管理 Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终，中间件可以将它们写入日志文件，数据库并通过网络发送。 内置渲染 Gin 为 JSON，XML 和 HTML 渲染提供了易于使用的 API。 可扩展性 新建一个中间件非常简单。 Gin 的官网： https://gin-gonic.com/zh-cn/\nGin Github 地址： https://github.com/gin-gonic/gin\n快速使用 下载并安装 gin：\n1 $ go get -u github.com/gin-gonic/gin 注意：go版本要 1.6 及以上。\n快速使用示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package main // 导入gin包 import \u0026#34;github.com/gin-gonic/gin\u0026#34; // 入口函数 func main() { // 初始化一个http服务对象，创建一个默认的路由引擎对象 r := gin.Default() // 配置路由 // 设置一个get请求的路由，url为/ping, 处理函数（或者叫控制器函数、回调函数）是一个闭包函数。 r.GET(\u0026#34;/ping\u0026#34;, func(c *gin.Context) { // 通过请求上下文对象Context, 直接往客户端返回一个json c.JSON(200, gin.H{ \u0026#34;message\u0026#34;: \u0026#34;pong\u0026#34;, }) }) err := r.Run() // 启动 HTTP 服务，默认在 0.0.0.0:8080 启动服务 if err != nil { panic(\u0026#34;Http serve start error:\u0026#34; + err.Error()) } } 运行命令go run main.go，即可启动http服务。然后就可以通过localhost:8080/ping 访问了。会返回如下内容：\n1 2 3 { \u0026#34;message\u0026#34;: \u0026#34;pong\u0026#34; } 注意：如果不期望在测试的时候，每次都弹出防火墙警告，可将监听地址、端口改为localhost:8080。生产环境应该为:port，省略地址，默认为0.0.0.0。\nGin框架热重载 所谓热重载就是当我们对代码进行修改时，程序能够自动重新加载并执行，这在我们开发中是非常便利的，可以快速进行代码测试，省去了每次手动重新编译。\nGin框架并没有提供热重载的功能，这个时候我们要实现热重载就要借助第三方的工具。\n这里推荐一个使用最多的gin框架热重载工具：air。它是在 fresh 的基础上诞生的。\n特性：\n彩色的日志输出 自定义构建或必要的命令 支持外部子目录 在 Air 启动之后，允许监听新创建的路径 更棒的构建过程 原理大致是：监听到文件系统修改通知后，重新构建应用程序。\nair基本使用 安装：\n1 go install github.com/cosmtrek/air@latest 注意：go版本要1.22 或更高。\n安装成功之后，就可以在gin项目根目录下直接运行air命令，就可以启动gin项目，实现热重载。\nair默认情况下使用默认配置启动服务。如果要修改默认配置，可以运行air init生成默认的配置文件.air.toml，修改之后，运行air即可使用修改的配置启动服务，注意：如果修改了配置文件名，需要用-c选项指定新的配置文件。否则将使用默认的.air.toml启动服务。\n一般使用默认配置就够用了。可修改的内容如下：\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 45 46 47 48 49 50 51 root = \u0026#34;.\u0026#34; testdata_dir = \u0026#34;testdata\u0026#34; tmp_dir = \u0026#34;tmp\u0026#34; [build] args_bin = [] bin = \u0026#34;tmp\\\\main.exe\u0026#34; cmd = \u0026#34;go build -o ./tmp/main.exe .\u0026#34; delay = 1000 exclude_dir = [\u0026#34;assets\u0026#34;, \u0026#34;tmp\u0026#34;, \u0026#34;vendor\u0026#34;, \u0026#34;testdata\u0026#34;] exclude_file = [] exclude_regex = [\u0026#34;_test.go\u0026#34;] exclude_unchanged = false follow_symlink = false full_bin = \u0026#34;\u0026#34; include_dir = [] include_ext = [\u0026#34;go\u0026#34;, \u0026#34;tpl\u0026#34;, \u0026#34;tmpl\u0026#34;, \u0026#34;html\u0026#34;] include_file = [] kill_delay = \u0026#34;0s\u0026#34; log = \u0026#34;build-errors.log\u0026#34; poll = false poll_interval = 0 post_cmd = [] pre_cmd = [] rerun = false rerun_delay = 500 send_interrupt = false stop_on_error = false [color] app = \u0026#34;\u0026#34; build = \u0026#34;yellow\u0026#34; main = \u0026#34;magenta\u0026#34; runner = \u0026#34;green\u0026#34; watcher = \u0026#34;cyan\u0026#34; [log] main_only = false time = false [misc] clean_on_exit = false [proxy] app_port = 0 enabled = false proxy_port = 0 [screen] clear_on_rebuild = false keep_scroll = true 基本使用如上，更多内容参考air官方。\n项目结构 实际项目业务功能和模块会很多，我们不可能把所有代码都写在一个go文件里面或者写在一个main入口函数里面；我们需要对项目结构做一些规划，方便维护代码以及扩展。\nGin框没有对项目结构做出限制，我们可以根据自己项目需要自行设计。\n这里给出一个典型的MVC框架大致的项目结构的例子，大家可以参考下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ├── conf #项目配置文件目录 │ └── config.toml #大家可以选择自己熟悉的配置文件管理工具包例如：toml、xml等等 ├── controllers #控制器目录，按模块存放控制器（或者叫控制器函数），必要的时候可以继续划分子目录。 │ ├── food.go │ └── user.go ├── main.go #项目入口，这里负责Gin框架的初始化，注册路由信息，关联控制器函数等。 ├── models #模型目录，负责项目的数据存储部分，例如各个模块的Mysql表的读写模型。 │ ├── food.go │ └── user.go ├── static assets #静态资源目录，包括Js，css，jpg等等，可以通过Gin框架配置，直接让用户访问。 │ ├── css │ ├── images │ └── js ├── logs #日志文件目录，主要保存项目运行过程中产生的日志。 └── views templates #视图模板目录，存放各个模块的视图模板，当然有些项目只有api，是不需要视图部分，可以忽略这个目录 └── index.html routers Gin框架运行模式 为方便调试，Gin 框架在运行的时候默认是debug模式，在控制台默认会打印出很多调试日志，上线的时候我们需要关闭debug模式，改为release模式。\n设置Gin框架运行模式：\n通过环境变量设置GIN_MODE，如：\n1 export GIN_MODE=release GIN_MODE环境变量，可以设置为debug或者release，默认为debug。\n通过代码设置:\n1 2 3 4 5 // 在main函数，初始化gin框架的时候执行下面代码 // 设置 release模式 gin.SetMode(gin.ReleaseMode) // 或者 设置debug模式 gin.SetMode(gin.DebugMode) 路由与控制器 Gin框架中的路由是指通过HTTP请求的路径找到对应的控制器函数（也可以叫处理器函数）。Gin框架的路由是基于httprouter包实现的。\n控制器函数主要负责处理http请求和响应请求。\n一个简单的例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 r := gin.Default() // 创建默认的路由引擎对象 // 配置路由：post请求, uri路径为：/user/login, 绑定doLogin控制器函数 r.POST(\u0026#34;/user/login\u0026#34;, doLogin) // 控制器函数 func doLogin(c *gin.Context) { // 获取post请求参数 username := c.PostForm(\u0026#34;username\u0026#34;) password := c.PostForm(\u0026#34;password\u0026#34;) // 通过请求上下文对象Context, 直接往客户端返回一个字符串 c.String(200, \u0026#34;username=%s,password=%s\u0026#34;, username, password) } 路由规则 一条路由规则由三部分组成：\nhttp请求方法 url路径 控制器函数 http请求方法 常用的http请求方法有下面4种:\nGET：查Read-select POST：增Create-insert PUT：改Update-update DELETE: 删Delete-delete 目前常用的API风格是RESTful风格。\n在RESTful风格中，每个路径表示不同的资源，通过不同的请求方式访问相同的路径表示执行不同的操作（CRUD)。\nRESTful CRUD示例：\n1 2 3 4 5 POST /users # 创建一个新用户 GET /users/1 # 获取ID为1的用户 PUT /users/1 # 更新ID为1的用户 DELETE /users/1 # 删除ID为1的用户 # 注意：1是路径参数 url路径 Gin框架中，url路径有三种写法：\n静态url路径 带路径参数的url路径 带星号（*）模糊匹配参数的url路径 下面看下各种url路由的例子\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 // 例子1， 静态Url路径, 即不带任何参数的url路径 /users/center /user/111 /food/12 // 例子2，带路径参数的url路径，url路径上面带有参数,参数由冒号（:）跟着一个字符串定义。 // 路径参数值可以是数值，也可以是字符串 //定义参数:id， 可以匹配/user/1, /user/899 /user/xiaoli 这类Url路径 /user/:id //定义参数:id， 可以匹配/food/2, /food/100 /food/apple 这类Url路径 /food/:id //定义参数:type和:page， 可以匹配/foods/2/1, /food/100/25 /food/apple/30 这类Url路径 /foods/:type/:page // 例子3. 带星号（*）模糊匹配参数的url路径 // 星号代表匹配任意路径的意思, 必须在*号后面指定一个参数名，后面可以通过这个参数获取*号匹配的内容。 //以/foods/ 开头的所有路径都匹配 //匹配：/foods/1， /foods/200, /foods/1/20, /foods/apple/1 /foods/*path //可以通过path参数获取*号匹配的内容。 控制器函数 控制器函数定义：\n1 type HandlerFunc func(*Context) 控制器函数接受一个上下文参数。 可以通过上下文参数，获取http请求参数，响应http请求。\n配置路由示例 配置路由就是利用路由规则定义路由：\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 //实例化gin实例对象。创建默认的路由引擎对象。 r := gin.Default() // 配置路由 //定义post请求, url路径为：/users, 绑定saveUser控制器函数 r.POST(\u0026#34;/users\u0026#34;, saveUser) //定义get请求，url路径为：/users/:id （:id是参数，例如: /users/10, 会匹配这个url模式），绑定getUser控制器函数 r.GET(\u0026#34;/users/:id\u0026#34;, getUser) //定义put请求 r.PUT(\u0026#34;/users/:id\u0026#34;, updateUser) //定义delete请求 r.DELETE(\u0026#34;/users/:id\u0026#34;, deleteUser) //控制器函数实现 func saveUser(c *gin.Context) { ...忽略实现... } func getUser(c *gin.Context) { ...忽略实现... } func updateUser(c *gin.Context) { ...忽略实现... } func deleteUser(c *gin.Context) { ...忽略实现... } 提示：实际项目开发中不要把路由定义和控制器函数都写在一个go文件，不方便维护，可以参考第一章的项目结构，规划自己的业务模块。\n路由分组 在做api开发的时候，如果要支持多个api版本，我们可以通过分组路由来实现api版本处理。\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func main() { router := gin.Default() // 创建v1组 v1 := router.Group(\u0026#34;/v1\u0026#34;) { // 在v1这个分组下，注册路由 v1.POST(\u0026#34;/login\u0026#34;, loginEndpoint) v1.POST(\u0026#34;/submit\u0026#34;, submitEndpoint) v1.POST(\u0026#34;/read\u0026#34;, readEndpoint) } // 创建v2组 v2 := router.Group(\u0026#34;/v2\u0026#34;) { // 在v2这个分组下，注册路由 v2.POST(\u0026#34;/login\u0026#34;, loginEndpoint) v2.POST(\u0026#34;/submit\u0026#34;, submitEndpoint) v2.POST(\u0026#34;/read\u0026#34;, readEndpoint) } router.Run(\u0026#34;:8080\u0026#34;) } 上面的例子将会注册下面的路由信息：\n/v1/login /v1/submit /v1/read /v2/login /v2/submit /v2/read 路由分组，其实就是设置了同一类路由的url前缀。\n利用路由分组，我们可以实现将需要授权和不需要授权的API进行分组管理(后面介绍）。同时也能够将他们分文件保存。\n示例如下：\nrouters/router.go:\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 45 46 47 48 49 50 package routers import ( \u0026#34;log\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) // registerRouterGroupFunc 定义类型别名，用于注册路由组的函数 type registerRouterGroupFunc = func(r *gin.Engine) // 存储所有需要注册的路由组函数 var routerGroups []registerRouterGroupFunc // 添加路由组函数到路由组列表中 func addRouterGroup(r registerRouterGroupFunc) { if r == nil { return } routerGroups = append(routerGroups, r) } // 注册所有路由组到主路由组中 func registerRoutes(r *gin.Engine) { for _, register := range routerGroups { register(r) } } // 加载所有路由组 func loadRouterGroups() { loadDefaultRouter() loadManageRouter() } // InitRouter 初始化主路由器 func InitRouter() { // 创建默认的 Gin 路由器 r := gin.Default() // 加载并注册所有路由组 loadRouterGroups() registerRoutes(r) // 启动服务器 err := r.Run(\u0026#34;localhost:8080\u0026#34;) if err != nil { log.Panic(\u0026#34;Start http serve error:\u0026#34;, err) } } routers/defaultRouter.go:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package routers import ( \u0026#34;github.com/arlettebrook/learn/controllers\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) // 加载所有默认的路由组 // 这里控制器函数没有分组保存 func loadDefaultRouter() { addRouterGroup(func(r *gin.Engine) { // defaultRouter := r.group(\u0026#34;/\u0026#34;) 可以省略 // defaultRouter.Get(...) 与下面等效 r.GET(\u0026#34;/\u0026#34;, func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ \u0026#34;msg\u0026#34;: \u0026#34;Hello, world!\u0026#34;, }) }) }) } routers/manageRouter.go:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package routers import ( \u0026#34;github.com/arlettebrook/learn/controllers\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) // 加载所有管理路由组 func loadManageRouter() { addRouterGroup(func(r *gin.Engine) { manageRouter := r.Group(\u0026#34;/manage\u0026#34;) { manageRouter.GET(\u0026#34;/addUser\u0026#34;, func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ \u0026#34;msg\u0026#34;: \u0026#34;Add user\u0026#34;, }) }) } }) } 在main.go中调用routers.InitRouter()即可启动gin框架。浏览器中访问http://localhost:8080/manage/addUser、 http://localhost:8080/就可以访问注册的两条路由。\n注意：默认路由组,在/,意思就是：r.Group(\u0026quot;/\u0026quot;)与r.Get(\u0026quot;/\u0026quot;,...)属于同一组。\n控制器函数分组 当我们的项目比较大的时候有必要对我们的控制器进行分组。\n参考路由分组示例，使用控制器函数分组可修改为：\nrouters/defaultRouter.go:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package routers import ( \u0026#34;github.com/arlettebrook/learn/controllers\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) func loadDefaultRouter() { addRouterGroup(func(r *gin.Engine) { defaultController := controllers.DefaultController{} r.GET(\u0026#34;/\u0026#34;, defaultController.Index) }) } routers/manageRouter.go:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package routers import ( \u0026#34;github.com/arlettebrook/learn/controllers\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) func loadManageRouter() { addRouterGroup(func(r *gin.Engine) { manageRouter := r.Group(\u0026#34;/manage\u0026#34;) manageController := controllers.ManageController{} { manageRouter.GET(\u0026#34;/addUser\u0026#34;, manageController.AddUser) } }) } 添加的控制器分组：\ncontrollers/defaultController.go:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package controllers import ( \u0026#34;net/http\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) type DefaultController struct { } func (d DefaultController) Index(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ \u0026#34;msg\u0026#34;: \u0026#34;Hello, world!\u0026#34;, }) } controllers/manageController.go:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package controllers import ( \u0026#34;net/http\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) type ManageController struct { } func (m ManageController) AddUser(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ \u0026#34;msg\u0026#34;: \u0026#34;Add user\u0026#34;, }) } 注意：为什么要在控制器里面定义对应的结构体对象呢？\n防止在同一个包下出现同名方法。 更加语义化，方便调用。 可以实现控制器继承。 控制器继承 控制器继承，可以将一些共用的控制器函数封装出来，实现代码优化。\n示例：\ncontrollers/baseController.go:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package controllers import ( \u0026#34;net/http\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) type BaseController struct { } func (b *BaseController) Ok(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{ \u0026#34;msg\u0026#34;: \u0026#34;Success!\u0026#34;, }) } func (b *BaseController) Fail(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{ \u0026#34;msg\u0026#34;: \u0026#34;Fail!\u0026#34;, }) } controllers/defaultController.go:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package controllers import ( \u0026#34;net/http\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) type DefaultController struct { BaseController } func (d DefaultController) Index(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ \u0026#34;msg\u0026#34;: \u0026#34;Hello, world!\u0026#34;, }) } routers/defaultRouter.go:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package routers import ( \u0026#34;github.com/arlettebrook/learn/controllers\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) func loadDefaultRouter() { addRouterGroup(func(r *gin.Engine) { defaultController := controllers.DefaultController{} r.GET(\u0026#34;/\u0026#34;, defaultController.Index) r.GET(\u0026#34;/extend/s\u0026#34;, defaultController.Ok) r.GET(\u0026#34;/extend/f\u0026#34;, defaultController.Fail) }) } 所以，我们只需要对应的控制器继承BaseController，那么该控制器就有对应的公有控制器函数。\nTODO: 利用控制器，还能实现更多的操作，未完待续\u0026hellip;\n补充：上面的示例中DefaultController 是无状态的（stateless），即它不在成员变量中保存与特定请求相关的数据，那么是线程安全的，不会发生数据冲突。然而，如果 DefaultController 有状态（stateful），即它在成员变量中保存与请求相关的数据，那么多个用户请求同时访问时可能会发生数据冲突。\n以下是一些确保线程安全的方法：\n无状态控制器: 确保控制器不保存任何与请求相关的状态信息。所有的状态信息应保存在请求的上下文中或传递给方法中的局部变量。\n使用中间件或局部变量: 如果需要保存请求相关的数据，将这些数据保存在请求的上下文中或方法的局部变量中，而不是控制器的成员变量中。 每次请求创建新的控制器实例: 在路由器中为每个请求创建一个新的控制器实例。这样每个请求都有自己的控制器实例，不会发生数据冲突。\n1 2 3 4 r.GET(\u0026#34;/\u0026#34;, func(ctx *gin.Context) { defaultController := controllers.DefaultController{} defaultController.Index(ctx) }) 获取请求参数 本章介绍Gin框架获取请求参数的方式。\n获取查询参数 查询（query）参数是url路径中?后面的所有键值对，他们用\u0026amp;符合连接。url例子：/query?id=1234\u0026amp;name=Manu\u0026amp;value=111\nGin框架获取查询参数的常用函数：\nfunc (c *Context) Query(key string) string 键不存在返回空字符串。 func (c *Context) DefaultQuery(key, defaultValue string) string 键不存在，使用默认值。 func (c *Context) GetQuery(key string) (string, bool) 键存在返回值和true。键不存在返回空字符串和false。键的值为空字符串也返回空字符串和false。 注意：\n获取的查询参数类型都为string。\n通常情况下查询参数出现在GET请求当中，因为GET请求参数会出现在url路径中。当然在POST请求中也能够获取查询参数。\n示例：/user?uid=20\u0026amp;page=1\n1 2 3 4 5 router.GET(\u0026#34;/user\u0026#34;, func(c *gin.Context) { uid := c.Query(\u0026#34;uid\u0026#34;) page := c.DefaultQuery(\u0026#34;page\u0026#34;, \u0026#34;0\u0026#34;) c.String(200, \u0026#34;uid=%v page=%v\u0026#34;, uid, page) }) 获取路径参数 路径（param）参数是包含在url路径中的参数，通常在url中不容易区分，只有在绑定路径参数的地方才能正确判断。\n绑定路径参数的语法是/:+参数名，如/user/:id，就是在user路径下绑定一个id参数，那么/user/1, /user/2\u0026hellip;, user后面的一级都是路径参数。\n当然路径参数也支持嵌套，如：/user/:id/:name/:age, /user/001/jack/18\n获取路径参数常用函数：\nfunc (c *Context) Param(key string) string 获取路径中指定位置的参数，键不存在，返回空字符串。 注意：键名要与绑定的路径参数名一直，没有冒号。位置要一一对应，否则请求会404. 通常获取GET请求中的路径参数，但也能获取POST请求中的路径参数。 获取路径参数只有这一个方法。 示例：域名/user/20\n1 2 3 4 r.POST(\u0026#34;/user/:uid\u0026#34;, func(c *gin.Context) { uid := c.Param(\u0026#34;uid\u0026#34;) c.String(200, \u0026#34;userID=%s\u0026#34;, uid) }) 获取表单参数 表单参数是位于请求体中的，所以只适用于POST请求。\n所以一般说获取表单参数就是获取POST请求参数。\n获取Post请求参数的常用函数：\nfunc (c *Context) PostForm(key string) string 从请求体（表单）中获取指定键的值，不存在返回空字符串。 func (c *Context) DefaultPostForm(key, defaultValue string) string 键不存在，指定默认值。 func (c *Context) GetPostForm(key string) (string, bool) 键不存在返回空字符串和false，存在返回值和true。 注意： 获取的参数类型但是string。 以上方法不能获取请求体中的json、xml等原始数据。只能获取表单数据。 获取原始数据，可以通过参数与结构体对象绑定获取。 示例：\n1 2 3 4 5 6 7 8 9 10 11 r.POST(\u0026#34;/doAddUser\u0026#34;, func(c *gin.Context) { username := c.PostForm(\u0026#34;username\u0026#34;) password := c.PostForm(\u0026#34;password\u0026#34;) age := c.DefaultPostForm(\u0026#34;age\u0026#34;, \u0026#34;20\u0026#34;) c.JSON(200, gin.H{ \u0026#34;usernmae\u0026#34;: username, \u0026#34;password\u0026#34;: password, \u0026#34;age\u0026#34;: age, }) }) 将请求参数绑定到结构体对象 前面获取参数的方式都是一个个参数的读取，比较麻烦，Gin框架支持根据请求参数类型自动绑定到一个struct对象。\n用到的方法是Bind和ShouldBind：\n二者会根据请求参数的类型自动选择对应的绑定引擎进行绑定\n根据请求头的MIME类型判断。 支持查询、表单、请求体原始数据（常用：xml、json）参数，不支持路径参数。\n如果需要将路径参数绑定到结构体，需要指定绑定引擎为XxxUri，并且用uri标签指定字段。 也可指定对应的绑定引擎就行绑定，如：\nBindQuery、ShouldBindQuery。\nBindJSON、ShouldBindJSON。\n1 2 c.ShouldBindBodyWithJSON() c.ShouldBindJSON() 二者都是将请求体中的json数据绑定到结构体对象\n区别是：使用ShouldBindBodyWithJSON绑定后，请求体内容不会被消耗，请求体的内容还能用ShouldBindBodyWithJSON重复使用，而其他的绑定方法只能绑定一次，会消耗请求体内容，读取后请求体内容不可再用。\n如果请求体只读取一次，使用ShouldBindJSON性能更好。读取多次只能用ShouldBindBodyWithJSON。如在中间件中进行验证后再处理业务逻辑。\n一句话带Body的可以使用相同方法重复绑定。没有的只能绑定一次。再次绑定会报EOF错误。\nBindUri、ShouldBindUri。\n绑定路径参数，注意需要用uri标签指定字段。 \u0026hellip;\n绑定的结构体字段需要与参数的键一直，才能绑定成功。如果不一致需要用结构体标签指明。格式为参数类型:键名，常用的绑定标签：\nform: 指定表单参数和查询参数的键。 json：指定json参数的键。 xml：指定xml参数的键。 uri：指定路径参数的键。 binding：指定绑定验证，如果验证失败，绑定会失败。多个属性用,分隔。常用属性： 必填字段（required）：确保字段在请求中必须存在且不能为空。 最小长度（min）: 确保字符串或数组的长度不小于指定值。 最大长度（max）: 确保字符串或数组的长度不大于指定值。 正则表达式（regexp）: 确保字符串符合指定的正则表达式。 1 2 3 4 type User struct { Id int `form:\u0026#34;id\u0026#34; json:\u0026#34;id\u0026#34; xml:\u0026#34;id\u0026#34; uri:\u0026#34;id\u0026#34; binding:\u0026#34;required\u0026#34;` Name string `form:\u0026#34;name\u0026#34; json:\u0026#34;name\u0026#34; xml:\u0026#34;name\u0026#34; uri:\u0026#34;name\u0026#34;` } Id字段在表单和查询参数中的键是id，json数据中的键是id，xml数据中的键是id，路径参数中的键是id，并且该字段在请求参数中必须存在且不为空，才能绑定成功。Name字段同理。\nbinding 标签通常与其他标签（如 json、form 等）结合使用，以指定键名和绑定验证。 二者区别：\nBind绑定结构体失败，会自动响应400错误：Bad Request ShouldBind绑定失败不会自动响应错误，需要手动处理响应错误。 一般推荐使用ShouldBind，更容易定制化。 示例：\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 package main import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34; ) type User struct { Username string `json:\u0026#34;username\u0026#34; binding:\u0026#34;required,min=3,max=20\u0026#34;` Password string `json:\u0026#34;password\u0026#34; binding:\u0026#34;required,min=8\u0026#34;` Email string `json:\u0026#34;email\u0026#34; binding:\u0026#34;required,email\u0026#34;` Age int `json:\u0026#34;age\u0026#34; binding:\u0026#34;required,min=18,max=65\u0026#34;` } func main() { r := gin.Default() r.POST(\u0026#34;/register\u0026#34;, func(c *gin.Context) { var user User if err := c.ShouldBindJSON(\u0026amp;user); err != nil { c.JSON(http.StatusBadRequest, gin.H{\u0026#34;error\u0026#34;: err.Error()}) return } c.JSON(http.StatusOK, gin.H{\u0026#34;status\u0026#34;: \u0026#34;registration successful\u0026#34;}) }) r.Run(\u0026#34;:8080\u0026#34;) } 在这个示例中：\nUsername 字段必须存在且长度在 3 到 20 之间。 Password 字段必须存在且长度至少为 8。 Email 字段必须存在且符合 email 格式。 Age 字段必须存在且值在 18 到 65 之间。 Gin如何获取客户ip 1 2 3 4 5 r.GET(\u0026#34;/ip\u0026#34;, func(c *gin.Context) { // 获取用户IP ip := c.ClientIP() remoteIP := c.RemoteIP() }) ClientIP方法会尽力返回客户端真实ip。会考虑代理的情况。\nRemoteIP方法返回与服务器直接连接的ip，不考虑代理的情况。\n区别（了解）：\n数据来源： RemoteIP() 直接从 c.Request.RemoteAddr 获取 IP 地址。 ClientIP() 优先从请求头（如 X-Forwarded-For、X-Real-IP 等）中获取 IP 地址，如果请求头中没有这些字段，则退回到 RemoteAddr。 使用场景： RemoteIP() 更适用于需要获取直接连接到服务器的客户端 IP 地址的场景。 ClientIP() 更适用于需要获取经过代理服务器后的客户端真实 IP 地址的场景，适用于有反向代理或负载均衡器的环境。 处理代理： RemoteIP() 不考虑代理的情况，只返回直接连接的客户端 IP。 ClientIP() 考虑代理的情况，优先从请求头获取客户端的真实 IP，适用于处理经过多个代理的请求。 获取文件上传参数 参考Gin文件上传。\n响应请求参数 本章介绍处理完http请求后如何响应请求，Gin框架支持以字符串、json、xml、文件等格式响应请求。\ngin.Context上下文对象支持多种返回处理结果，下面分别介绍不同的响应方式。\n以字符串格式响应请求 通过String方法返回字符串。\n方法定义：\n1 func (c *Context) String(code int, format string, values ...interface{}) 参数说明：\n参数 说明 code http状态码 format 返回结果，支持类似Sprintf函数一样的字符串格式定义，例如,%d 代表插入整数，%s代表插入字符串 values 任意个format参数定义的字符串格式参数 示例：\n1 2 3 4 r.GET(\u0026#34;/news\u0026#34;, func(c *gin.Context) { aid := c.Query(\u0026#34;aid\u0026#34;) c.String(http.StatusOK, \u0026#34;aid=%s\u0026#34;, aid) }) 提示： net/http包定义了多种常用的状态码常量，例如：http.StatusOK == 200， http.StatusMovedPermanently == 301， http.StatusNotFound == 404, http.StatusBadRequest == 400等，具体可以参考net/http包\n以json格式响应请求 我们开发api接口的时候常用的格式就是json。\n通过JSON方法返回json数据。\n方法定义：\n1 func (c *Context) JSON(code int, obj any) 参数说明：\ncode：响应的http状态码。 obj：响应的json数据。类型是可以序列化为json类型的数据，如：结构体对象、map[string]any。Gin框架会自动将它们序列化为JSON数据，并将它们放在请求体当中。 Gin框架为方便将map数据作为json响应，提供了一个快捷类型：type H map[string]any，gin.H是map[string]any类型。 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type User struct { Id int `form:\u0026#34;id\u0026#34; json:\u0026#34;id\u0026#34; xml:\u0026#34;id\u0026#34; uri:\u0026#34;id\u0026#34; binding:\u0026#34;required\u0026#34;` Name string `form:\u0026#34;name\u0026#34; json:\u0026#34;name\u0026#34; xml:\u0026#34;name\u0026#34; uri:\u0026#34;name\u0026#34;` } func main() { r := gin.Default() // gin.H 是 map[string]interface{}的缩写 r.GET(\u0026#34;/someJSON\u0026#34;, func(c *gin.Context) { // 方式一：自己拼接 JSON c.JSON(http.StatusOK, gin.H{\u0026#34;message\u0026#34;: \u0026#34;Hello world!\u0026#34;}) // {\u0026#34;messgae\u0026#34;:\u0026#34;Hello world!\u0026#34;} }) r.GET(\u0026#34;/moreJSON\u0026#34;, func(c *gin.Context) { // 方法二：使用结构体 u := models.User{Id: 123, Name: \u0026#34;Marry\u0026#34;} c.JSON(http.StatusOK, u) // {\u0026#34;id\u0026#34;:123,\u0026#34;name\u0026#34;:\u0026#34;Marry\u0026#34;} }) r.Run(\u0026#34;localhost:8080\u0026#34;) } 以带填充的json格式响应请求（了解） 用到的方法是JSONP，英文名为：JSON with Padding，中文大概：带填充的json。\n只有当请求中指定查询参数callback时，才会响应带填充的json，否则，只响应json。 示例： 请求：http://localhost:8080/get?callback=abc，响应：abc({json数据})。 Content-Type：application/javascript; charset=utf-8 请求：http://localhost:8080/get，响应：{json数据}。 Content-Type：application/json; charset=utf-8 作用：\nJSONP 是一种古老的跨域请求解决方案，通过 \u0026lt;script\u0026gt; 标签加载和执行 JavaScript 代码来实现数据传输，因此只适用于 GET 请求的简单跨域场景。 因为要配合script标签的src属性使用，它只能发送GET请求。 带填充的json格式就是JavaScript代码。 具体使用：\n在前端定义好回调函数名。\n通过\u0026lt;script\u0026gt; 标签的 src 属性指向跨域的服务器，并包含一个查询参数 callback，其值是一个回调函数的名字。\n服务器收到请求后，将数据包装在回调函数中，生成一段 JavaScript 代码，并返回给客户端。\n浏览器加载并执行返回的 JavaScript 代码，从而调用回调函数，回调函数接收数据并处理。\n示例：\n1 2 3 4 5 6 7 8 9 r.GET(\u0026#34;/get\u0026#34;, func(c *gin.Context) { data := map[string]interface{}{ \u0026#34;message\u0026#34;: \u0026#34;Hello, World!\u0026#34;, \u0026#34;status\u0026#34;: \u0026#34;success\u0026#34;, } c.JSONP(http.StatusOK, gin.H{ \u0026#34;data\u0026#34;: data, }) }) http://localhost:8080/get?callback=abc响应：\n1 2 3 4 5 6 abc({ \u0026#34;data\u0026#34;: { \u0026#34;message\u0026#34;: \u0026#34;Hello, World!\u0026#34;, \u0026#34;status\u0026#34;: \u0026#34;success\u0026#34; } }) http://localhost:8080/get响应：\n1 2 3 4 5 6 { \u0026#34;data\u0026#34;: { \u0026#34;message\u0026#34;: \u0026#34;Hello, World!\u0026#34;, \u0026#34;status\u0026#34;: \u0026#34;success\u0026#34; } } 注意事项：\n存在数据安全问题：由于 JSONP 是通过执行返回的 JavaScript 代码来实现数据传输的，如果不对返回的数据进行验证，可能会执行恶意代码。\n支持有限：JSONP 只支持 GET 请求，不支持其他 HTTP 方法（如 POST、PUT、DELETE 等）。因此，它只适用于获取数据的场景。\n推荐使用现代跨域解决方案：CORS（Cross-Origin Resource Sharing）跨域资源共享，允许浏览器向跨域服务器发送请求并接受响应。通过设置适当的 HTTP 头，服务器可以指示浏览器允许跨域请求，不受同源策略控制，报错。\n就是设置对应的响应头（跨域资源共享头cors头），告诉浏览器允许跨域请求。\n主要的 CORS 头部：\nAccess-Control-Allow-Origin：指定允许的源。 * 表示允许所有源。\nAccess-Control-Allow-Methods：指定允许的 HTTP 方法。\nAccess-Control-Allow-Headers：指定允许的请求头。\nAccess-Control-Allow-Credentials：指示是否允许发送凭证。\nAccess-Control-Expose-Headers：指定可以暴露的响应头。\nAccess-Control-Max-Age：指定预检请求结果的缓存时间。\n注意：以上头都只有在跨域请求中才会进行校验。\n示例：\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 package main import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34; ) func CORSMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set(\u0026#34;Access-Control-Allow-Origin\u0026#34;, \u0026#34;*\u0026#34;) c.Writer.Header().Set(\u0026#34;Access-Control-Allow-Methods\u0026#34;, \u0026#34;GET, POST, OPTIONS\u0026#34;) c.Writer.Header().Set(\u0026#34;Access-Control-Allow-Headers\u0026#34;, \u0026#34;Origin, Content-Type, Accept\u0026#34;) if c.Request.Method == \u0026#34;OPTIONS\u0026#34; { c.AbortWithStatus(http.StatusNoContent) return } c.Next() } } func main() { r := gin.Default() r.Use(CORSMiddleware()) r.GET(\u0026#34;/data\u0026#34;, func(c *gin.Context) { data := map[string]interface{}{ \u0026#34;message\u0026#34;: \u0026#34;Hello, World!\u0026#34;, \u0026#34;status\u0026#34;: \u0026#34;success\u0026#34;, } c.JSON(http.StatusOK, data) }) r.Run(\u0026#34;:8080\u0026#34;) } 本文后面会介绍Gin中间件。\n在Gin框架中，Gin 官方推荐使用 gin-contrib/cors 中间件来设置 CORS 头部。\n不出意外的话，后面会出一篇关于cors详细介绍以及gin-contrib/cors使用的文章：Cors Introduction。 以xml格式响应请求 通过XML方法返回xml数据\n方法定义：\n1 func (c *Context) XML(code int, obj any) 该方法与JSON方法类似。\n参数说明：\ncode：响应的http状态码。 obj：响应的xml数据。类型是能够序列化成xml数据的类型，如map[string]any、结构体对象。 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type User struct { Id int `form:\u0026#34;id\u0026#34; json:\u0026#34;id\u0026#34; xml:\u0026#34;id\u0026#34; uri:\u0026#34;id\u0026#34; binding:\u0026#34;required\u0026#34;` Name string `form:\u0026#34;name\u0026#34; json:\u0026#34;name\u0026#34; xml:\u0026#34;name\u0026#34; uri:\u0026#34;name\u0026#34;` } func main() { r := gin.Default() // gin.H 是 map[string]interface{}的缩写 r.GET(\u0026#34;/someXML\u0026#34;, func(c *gin.Context) { c.XML(http.StatusOK, gin.H{\u0026#34;message\u0026#34;: \u0026#34;Hello world!\u0026#34;}) // \u0026lt;map\u0026gt;\u0026lt;message\u0026gt;Hello world!\u0026lt;/message\u0026gt;\u0026lt;/map\u0026gt; }) r.GET(\u0026#34;/moreXML\u0026#34;, func(c *gin.Context) { // 方法二：使用结构体 u := models.User{Id: 123, Name: \u0026#34;Marry\u0026#34;} c.XML(http.StatusOK, u) // \u0026lt;User\u0026gt;\u0026lt;id\u0026gt;123\u0026lt;/id\u0026gt;\u0026lt;name\u0026gt;Marry\u0026lt;/name\u0026gt;\u0026lt;/User\u0026gt; }) r.Run(\u0026#34;localhost:8080\u0026#34;) } 以文件格式响应请求 下面介绍gin框架如何响应一个文件，可以用来做文件下载。\n响应文件用File或FileAttachment方法：\nFile方法： 直接响应文件，Gin框架会自动根据文件类型设置MIME类型，浏览器会自动根据MEM类型I处理文件（显示文件）。 只接收一个参数：为响应文件的本地系统路径。 FileAttachment方法： 直接下载文件，会弹出下载框。文件名为给定的第二个参数。 有两个参数：第一个参数与File的参数一直，第二个参数：指定文件下载的名称。 原理：FileAttachment会比File多设置一个响应头Content-Disposition:attachment; filename=\u0026quot;filename\u0026quot;，告诉浏览器将文件作为附加下载。\n设置http响应头（设置Header） 在Gin框架中：\n获取请求头的方法是func (c *Context) GetHeader(key string)。\n将请求头与结构体绑定的方法是BindHeader或ShouldBindHeader。\n二者区别是：BindHeader绑定失败，会自动响应400错误，而ShouldBind不会，需要手动响应错误。 设置响应头的方法是func (c *Context) Header(key, value string)：\n支持反复设置响应头。\n若键的值为空，将删除该响应头。\n设置响应头应该在响应前设置，响应后设置无效。\n源码：\n1 2 3 4 5 6 7 func (c *Context) Header(key, value string) { if value == \u0026#34;\u0026#34; { c.Writer.Header().Del(key) return } c.Writer.Header().Set(key, value) } HTML模板渲染 前面详细介绍了gin框架响应不同类型的参数，其实还漏掉了响应html模板渲染。因为涉及到go模板渲染，就单独拿出来介绍。\nGin框架默认封装了golang内置的html/template包用于处理html模版，如果你开发的是接口服务，不提供html页面可以跳过本章内容。\n作用：Gin框架内置的模板渲染功能，可以通过http请求，就可以生成安全的HTML内容，如动态网页、邮件内容、HTML报告等。同时简化了动态HTML页面的生成过程，通过设置模板目录和在路由处理函数中渲染模板，可以轻松地生成包含动态数据的网页。此功能有助于分离业务逻辑和表示层，提高代码的可维护性，并且通过自动HTML转义，提高了Web应用的安全性。\ngo模板渲染参考文章：《Template Introduction》\n下面主要介绍Gin框架封装的html/template模板渲染与内置的html/template模板渲染的区别。\n解析模板并创建模板对象：\n作用 Go内置 Gin封装 指定解析的模板文件 func (t *Template) ParseFiles(filenames ...string) (*Template, error) func (engine *Engine) LoadHTMLFiles(files ...string) 根据匹配模式解析模板文件（推荐） func (t *Template) ParseGlob(pattern string) (*Template, error) func (engine *Engine) LoadHTMLGlob(pattern string) 渲染模板对象，改成：\n1 func (c *Context) HTML(code int, name string, obj any) code：状态码。\nname：模板名称。\nobj：渲染的数据。\n其他主要区别：\n将语法分隔符Delims、自定义模板函数映射FuncMap等方法的对象改成了模板引擎对象engine *Engine，不需要我们在手动创建模板对象。\n自定义模板函数也不需要我们手动添加（Funcs）自定义模板函数映射FuncMap。\n其余的模板语法，模板嵌套，模板函数，自动HTML内容转义，重写模板等使用都与内置的html/template一模一样。\n示例：\ntemplates/default/hello.tmpl：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 {[ define \u0026#34;default/hello.tmpl\u0026#34; -]} \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta http-equiv=\u0026#34;X-UA-Compatible\u0026#34; content=\u0026#34;IE=edge\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;模板渲染\u0026lt;/h1\u0026gt; \u0026lt;h3\u0026gt;Your message is: {[ ToUpper . ]} \u0026lt;/h3\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; {[- end ]} main.go:\n1 2 3 4 5 6 7 8 9 10 11 12 r := gin.Default() r.Delims(\u0026#34;{[\u0026#34;, \u0026#34;]}\u0026#34;) r.FuncMap = template.FuncMap{ \u0026#34;ToUpper\u0026#34;: strings.ToUpper, } r.LoadHTMLGlob(\u0026#34;templates/**/*.tmpl\u0026#34;) r.GET(\u0026#34;/html\u0026#34;, func(c *gin.Context) { msg := c.Query(\u0026#34;msg\u0026#34;) c.HTML(http.StatusOK, \u0026#34;default/hello.tmpl\u0026#34;, msg) }) 启动http服务访问[http://localhost:8181/html?msg=Hello, worrld!](http://localhost:8181/html?msg=Hello, worrld!)将响应渲染之后的HTML内容。\n浏览器呈现为：\n1 2 模板渲染 Your message is: HELLO, WORRLD! 响应的html源码为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta http-equiv=\u0026#34;X-UA-Compatible\u0026#34; content=\u0026#34;IE=edge\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;模板渲染\u0026lt;/h1\u0026gt; \u0026lt;h3\u0026gt;Your message is: HELLO, WORRLD! \u0026lt;/h3\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 静态文件服务 当我们渲染的HTML文件中引用了静态文件时，就需要配置静态文件服务。以便js、css、jpg之类的静态文件能够正常响应。\n在Gin框架中访问静态资源文件通常是通过设置一个静态文件服务来实现的。Gin提供了一个非常方便的方法来处理这一任务。\n配置静态文件服务常用的两个方法：\n映射静态资源目录：\n1 2 // 设置静态文件夹，URL路径 /static 映射到文件系统中的 assets 文件夹 router.Static(\u0026#34;/static\u0026#34;, \u0026#34;./assets\u0026#34;) router.Static(\u0026quot;/static\u0026quot;, \u0026quot;./static\u0026quot;)方法的第一个参数是URL路径前缀，第二个参数是文件系统中的路径。当访问http://localhost:8080/static/css/styles.css时，Gin会在文件系统的./assets/css/styles.css路径查找文件并返回给客户端。\n注意事项：URL路径前缀建议为一个静态资源路径/static，要避免路由重复。底层是通过/static/*filepath匹配的，如果为/将匹配所有的路径，后续配置的路径将失效，Gin服务启动也会失败。\n映射单个静态资源文件：\n1 2 // 可以使用 router.StaticFile 来设置单个文件的静态路径 router.StaticFile(\u0026#34;/favicon.ico\u0026#34;, \u0026#34;./assets/favicon.ico\u0026#34;) 第一个参数：访问路径。第二个参数：文件系统中的路径。\n对于/favicon.icoURL路径，是一个特殊的路径，用于请求浏览器标签页的图标。\n当我们使用浏览器访问任何网站时，浏览器会自动请求/favicon.ico路径，以便正常显示标签页图标。失败会显示一个地球。\n为了提升用户体验和网站的识别度，通常都会配置网站图标的静态资源映射。\n因此会用到映射单个静态资源文件StaticFile，注意不能使用映射静态资源目录，因为浏览器自动请求网站图标的路径是在根目录。\n提示：\n网站图标（标签页图标）很小，格式为ico(x-icon)，名为favicon，可以通过图标生成器生成。如：Favicon.ico图标在线生成器。\n默认的网页图标路径是域名/favicon，当然也可以自定义，通过link标签：\n1 2 \u0026lt;!-- 定义网页的favicon 下面的href属性默认为/favicon.ico --\u0026gt; \u0026lt;link rel=\u0026#34;icon\u0026#34; type=\u0026#34;image/x-icon\u0026#34; href=\u0026#34;//www.example.com/static/favicon.ico\u0026#34; \u0026gt; Gin中间件 在Gin框架中，中间件（Middleware）指的是可以拦截http请求-响应生命周期的特殊函数，在请求-响应生命周期中可以注册多个中间件，每个中间件执行不同的功能，一个中间执行完再轮到下一个中间件执行。\n通俗的讲：中间件就是匹配路由前和匹配路由后执行的一系列操作。\n中间件的常见应用场景如下：\n日志记录：记录每个请求的详细信息，例如请求方法、路径、状态码和处理时间。 认证和授权：检查请求是否包含有效的认证信息，决定是否允许访问资源。 错误处理：捕获和处理请求过程中发生的错误，返回统一的错误响应。 数据验证：在请求到达处理器之前对请求数据进行验证。 CORS处理：处理跨域资源共享（CORS）的相关设置，允许或拒绝跨域请求。、 限流：限制客户端在一定时间内的请求数量，防止过载。 使用中间件 定义中间件：\n中间件本质上是一个 Gin 处理函数（控制器函数：gin.HandlerFunc），接受 *gin.Context 作为参数。\n所以定义中间件可以定义一个函数，返回值是gin.HandlerFunc类型，即返回将*gin.Context 作为参数的函数即可。\n或者直接定义gin.HandlerFunc函数，跟定义控制器函数类似。\n例如，定义一个简单的日志中间件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func LoggerMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // 记录请求开始时间 startTime := time.Now() // 处理请求 c.Next() // 记录处理时间 duration := time.Since(startTime) log.Printf(\u0026#34;Request %s %s took %v\u0026#34;, c.Request.Method, c.Request.URL.Path, duration) } } Next方法是调用该路由剩余的控制器函数，当剩余的控制器函数执行完毕之后，继续执行后面的代码，起到放行的作用。\n​\t当存在多个中间件时，执行顺序依此类推。\n​\t与之相反的方法Abort，终止调用剩余的控制器函数，起到不放行的作用。\n​\t如果没有调用以上两个方法，会依次按照注册顺序执行控制器函数。\n所有控制器函数执行完毕之后，意味着本次请求处理完毕。因此上面演示了记录处理一次请求所花费的时间。（注意：要将其注册为第一个中间件。）\n使用中间件：\n全局中间件\n可以在创建路由器时使用 Use 方法注册全局中间件，这样所有请求都会经过该中间件。\n​\t注意：Use方法可以一次性注册一个或多个中间件。\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 r := gin.Default() // 注册全局中间件 r.Use(LoggerMiddleware()) // 定义路由 r.GET(\u0026#34;/ping\u0026#34;, func(c *gin.Context) { c.JSON(200, gin.H{ \u0026#34;message\u0026#34;: \u0026#34;pong\u0026#34;, }) }) r.Run() // 启动服务器 路由组中间件\n也可以在特定的路由组中使用中间件，这样只有指定组经过该中间件。例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 r := gin.Default() // 定义一个需要认证的路由组 authGroup := r.Group(\u0026#34;/auth\u0026#34;) authGroup.Use(AuthMiddleware()) // 注册中间件 { authGroup.GET(\u0026#34;/profile\u0026#34;, func(c *gin.Context) { c.JSON(200, gin.H{ \u0026#34;user\u0026#34;: \u0026#34;John Doe\u0026#34;, }) }) } r.Run() // 启动服务器 单独使用中间件\n如果只希望在某个特定路由上使用中间件，可以直接在定义路由时使用，这样只有该路由经过中间件。例如：\n1 2 3 4 5 6 7 8 9 10 r := gin.Default() // 单独为某个路由使用中间件 r.GET(\u0026#34;/secure\u0026#34;, AuthMiddleware(), func(c *gin.Context) { c.JSON(200, gin.H{ \u0026#34;message\u0026#34;: \u0026#34;secure\u0026#34;, }) }) r.Run() // 启动服务器 前面介绍了Gin中间件的本质就是控制器函数，因此配置路由时，最后一个控制器函数前面的控制器函数都是中间件。\n因此路由组中间件还可以写成：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 r := gin.Default() // 定义一个需要认证的路由组并注册中间件 authGroup := r.Group(\u0026#34;/auth\u0026#34;, AuthMiddleware()) // authGroup.Use(AuthMiddleware()) // 注册中间件 { authGroup.GET(\u0026#34;/profile\u0026#34;, func(c *gin.Context) { c.JSON(200, gin.H{ \u0026#34;user\u0026#34;: \u0026#34;John Doe\u0026#34;, }) }) } r.Run() // 启动服务器 中间件之间共享数据 中间件间与对应控制器之间共享数据是通过上下文的Set方法和Get方法实现的，存储的是键值对，key是string类型，value是any类型。共享范围仅在本次请求中。生命周期是本次请求结束，自动销毁。Gin框架没有提供主动删除共享数据的方法，可以在请求中将共享数据的值设为nil，起到删除的效果。\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // 模拟鉴权：获取username成功表示通过 username, f := c.GetPostForm(\u0026#34;username\u0026#34;) if !f { c.Abort() c.String(http.StatusUnauthorized, \u0026#34;未授权!\u0026#34;) return } c.Set(\u0026#34;username\u0026#34;, username) c.Next() } } 1 2 3 4 5 6 7 8 9 10 11 manageRouter := r.Group(\u0026#34;/manage\u0026#34;) manageRouter.Use(AuthMiddleware()) { manageRouter.POST(\u0026#34;/addUser\u0026#34;, func(c *gin.Context) { username, _ := c.Get(\u0026#34;username\u0026#34;) c.JSON(http.StatusOK, gin.H{ \u0026#34;msg\u0026#34;: \u0026#34;Add user\u0026#34;, \u0026#34;username\u0026#34;: username, }) }) } 内置中间件 Gin 提供了一些内置中间件，常用的有：\nLogger(): 日志中间件，记录请求日志。 注意：无论Gin是debug或者release模式，都会记录请求日志。 Recovery(): 恢复中间件，捕获任何恐慌（panic），并返回 500 错误。 gin.BasicAuth(): 基本认证中间件。 使用 gin.Default() 创建的默认路由引擎，默认使用了Logger和Recovery中间件。\n1 2 3 4 5 6 func Default(opts ...OptionFunc) *Engine { debugPrintWARNINGDefault() // 打印日志：记录使用的中间件以及go版本要求 engine := New() engine.Use(Logger(), Recovery()) return engine.With(opts...) } 如果不想使用上面两个默认的中间件，可以使用 gin.New() 新建一个没有任何默认中间件的路由引擎。\n使用方式与自定义中间件类似。例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 r := gin.New() // 使用内置的日志和恢复中间件 r.Use(gin.Logger()) r.Use(gin.Recovery()) r.GET(\u0026#34;/ping\u0026#34;, func(c *gin.Context) { c.JSON(200, gin.H{ \u0026#34;message\u0026#34;: \u0026#34;pong\u0026#34;, }) }) r.Run() // 启动服务器 上面示例与使用gin.Default()创建的路由引擎唯一区别是没有记录使用的中间件以及go版本要求。\n1 2 3 4 5 6 7 8 9 10 func debugPrintWARNINGDefault() { if v, e := getMinVer(runtime.Version()); e == nil \u0026amp;\u0026amp; v \u0026lt; ginSupportMinGoVer { debugPrint(`[WARNING] Now Gin requires Go 1.18+. `) } debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. `) } 中间件的执行顺序 Gin 中间件按照注册顺序执行。对于每个请求，所有中间件会按照注册的顺序依次执行 c.Next() 之前的代码，当一个中间件调用 c.Next() 时，控制权传递给下一个中间件，所有中间件执行完之后再按逆序执行 c.Next() 之后的代码。\n中间件中使用goroutine 当在中间件或控制器函数中启动新的goroutine时，不能使用原始的上下文（c *gin.Context），因为 gin.Context 不是线程安全的。在 goroutine 中直接访问 gin.Context 的字段可能会导致竞态条件（race condition）和不可预测的行为。\n中间件中使用 goroutine 的场景\n日志记录：异步记录日志以减少对请求处理时间的影响。 异步处理：在响应请求后执行耗时的任务，例如发送通知邮件、清理资源等。 并发处理：在请求处理中并发执行多个任务。 在中间件中使用 goroutine 的正确方式\n安全地传递上下文数据\n为了避免直接在 goroutine 中使用 gin.Context，我们可以在启动 goroutine 之前提取需要的数据，然后将这些数据传递给 goroutine。例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func AsyncLoggerMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // 提取需要的数据 method := c.Request.Method path := c.Request.URL.Path clientIP := c.ClientIP() // 启动一个 goroutine 进行异步日志记录 go func() { log.Printf(\u0026#34;Request %s %s from %s\u0026#34;, method, path, clientIP) }() // 继续处理请求 c.Next() } } 请求响应之后启动协程处理任务\n可以在响应客户端后启动 goroutine 执行耗时操作，例如发送邮件通知：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func AsyncTaskMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // 提取需要的数据 userEmail := c.Request.Header.Get(\u0026#34;User-Email\u0026#34;) // 继续处理请求 c.Next() // 在响应客户端后启动一个 goroutine 执行异步任务 go func() { // 模拟耗时任务，例如发送邮件 time.Sleep(5 * time.Second) log.Printf(\u0026#34;Email sent to %s\u0026#34;, userEmail) }() } } 创建上下文副本\n在 Gin 中使用 c.Copy() 方法可以在 goroutine 中安全地访问 gin.Context 的数据。c.Copy() 会创建并返回 gin.Context 的一个深拷贝，这样就可以避免竞态条件的问题。\n以下是一个使用 c.Copy() 的示例，演示如何在 Gin 中间件中使用 goroutine 进行异步处理。\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package main import ( \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) func AsyncLoggerMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // 复制上下文 cCp := c.Copy() // 启动一个 goroutine 进行异步日志记录 go func() { // 模拟日志记录的耗时操作 time.Sleep(2 * time.Second) log.Printf(\u0026#34;Request %s %s from %s\u0026#34;, cCp.Request.Method, cCp.Request.URL.Path, cCp.ClientIP()) }() // 继续处理请求 c.Next() } } func AsyncTaskMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // 复制上下文 cCp := c.Copy() // 继续处理请求 c.Next() // 在响应客户端后启动一个 goroutine 执行异步任务 go func() { // 模拟耗时任务，例如发送邮件 time.Sleep(5 * time.Second) userEmail := cCp.Request.Header.Get(\u0026#34;User-Email\u0026#34;) log.Printf(\u0026#34;Email sent to %s\u0026#34;, userEmail) }() } } func main() { r := gin.Default() // 注册中间件 r.Use(AsyncLoggerMiddleware()) r.Use(AsyncTaskMiddleware()) // 定义路由 r.GET(\u0026#34;/ping\u0026#34;, func(c *gin.Context) { c.JSON(200, gin.H{ \u0026#34;message\u0026#34;: \u0026#34;pong\u0026#34;, }) }) // 启动服务器 r.Run() } 通过使用 c.Copy()，可以安全地在 Gin 中间件中使用 goroutine，提升应用的并发处理能力和响应效率。\n​\n注意事项\n线程安全：避免在 goroutine 中直接访问 gin.Context 的字段，应该在启动 goroutine 之前提取所需数据。 资源泄漏：确保 goroutine 中的任务能够正常完成，避免因意外情况导致 goroutine 泄漏。 错误处理：在 goroutine 中处理可能发生的错误，以免影响主请求的处理流程。 使用深拷贝：确保在启动 goroutine 之前调用 c.Copy()，并在 goroutine 中使用返回的副本，而不是直接使用原始的 gin.Context。 通过合理使用 goroutine，可以显著提升应用的并发处理能力，同时保持请求的快速响应。\n小结 中间件在 Gin 框架中起着至关重要的作用，可以帮助开发者实现日志记录、认证授权、错误处理等常见功能。Gin 提供了灵活的中间件使用方式，可以全局、路由组或者单独路由使用中间件，满足不同的需求。通过合理使用中间件，可以大大提升应用的结构和代码复用性。\nGin文件上传 在Gin框架中，处理文件上传是一项常见的任务。\nGin是一个高效的Go语言Web框架，提供了简洁而强大的API来处理HTTP请求，包括文件上传。下面是关于如何在Gin框架中处理文件上传的相关知识和示例代码。\n基本文件上传 Gin提供了方便的方法来处理单个文件的上传。以下是一个示例代码：\n1 2 3 4 5 6 \u0026lt;form action=\u0026#34;/admin/user/doAdd\u0026#34; method=\u0026#34;post\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; 用户名： \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;username\u0026#34; placeholder=\u0026#34;用户名\u0026#34;\u0026gt; \u0026lt;br\u0026gt; \u0026lt;br\u0026gt; 头 像1：\u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;file1\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;br\u0026gt; 头 像2：\u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;file2\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;br\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;提交\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; 注意：需要在上传文件的 form 表单上面需要加入 enctype=\u0026ldquo;multipart/form-data\u0026rdquo;\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) func main() { r := gin.Default() r.POST(\u0026#34;/upload\u0026#34;, func(c *gin.Context) { file1, err := c.FormFile(\u0026#34;file1\u0026#34;) file2, err := c.FormFile(\u0026#34;file2\u0026#34;) if err != nil { c.String(http.StatusBadRequest, \u0026#34;Bad request: %s\u0026#34;, err) return } err = c.SaveUploadedFile(file1, path.Join(\u0026#34;./uploads\u0026#34;, file1.Filename)) err = c.SaveUploadedFile(file2, path.Join(\u0026#34;./uploads\u0026#34;, file2.Filename)) if err != nil { c.String(http.StatusInternalServerError, \u0026#34;Could not save file: %s\u0026#34;, err) return } c.String(http.StatusOK, \u0026#34;File %s and %s uploaded successfully.\u0026#34;, file1.Filename, file2.Filename) }) r.Run(\u0026#34;:8080\u0026#34;) } 在这个例子中，Gin通过c.FormFile(\u0026quot;file\u0026quot;)方法获取上传的文件。\n​\tFormFile方法返回指定表单键（只能返回第一个）的描述多部分表单请求的文件部分对象。\n​\t意思就是：根据多部分表单请求的文件部分的name属性获取文件部分对象multipart.FileHeader。\n然后使用c.SaveUploadedFile方法将文件保存到服务器的指定目录。\n​\tSaveUploadedFile方法接收两个参数：第一个：要保存的描述多部分表单请求的文件部分对象，第二个：本地系统路径（string类型）。\n上面演示了如何根据不同的表单键将上传的文件保存到服务器（即处理一个或多个文件上传，name属性不同），是根据表单的name属性保存的。适用于不同的name属性。\n注意，上面这种情况如果存在多个相同的name属性，只能获取第一个文件。\n下面介绍如何保存相同name属性的文件。适用于相同的name属性。\n处理多个文件上传 如果需要处理多个文件上传，name属性相同，可以使用c.MultipartForm方法。以下是示例代码：\n1 2 3 4 5 6 \u0026lt;form action=\u0026#34;/admin/user/doAdd\u0026#34; method=\u0026#34;post\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; 用户名： \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;username\u0026#34; placeholder=\u0026#34;用户名\u0026#34;\u0026gt; \u0026lt;br\u0026gt; \u0026lt;br\u0026gt; 头 像1：\u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;files\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;br\u0026gt; 头 像2：\u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;files\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;br\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;提交\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; 1 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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) func main() { r := gin.Default() r.POST(\u0026#34;/uploads\u0026#34;, func(c *gin.Context) { // 从请求中解析多个文件 form, err := c.MultipartForm() if err != nil { c.String(http.StatusBadRequest, \u0026#34;Bad request\u0026#34;) return } files := form.File[\u0026#34;files\u0026#34;] if len(files) != 0 { for _, file := range files { if err := c.SaveUploadedFile(file, path.Join(\u0026#34;./uploads\u0026#34;, file.Filename)); err != nil { c.String(http.StatusInternalServerError, \u0026#34;Could not save file: %s\u0026#34;, err) return } } } else { c.String(http.StatusBadRequest, \u0026#34;Bad Request: 请上传指定文件！\u0026#34;) } c.String(http.StatusOK, fmt.Sprintf(\u0026#34;%d files uploaded successfully.\u0026#34;, len(files))) }) r.Run(\u0026#34;:8080\u0026#34;) } 在这个例子中，首先使用c.MultipartForm方法获多部分表单数据，文件部分就在其File字段下，是map[string][]*FileHeader类型。通过表单键（name属性的值）就能获取该键下的所有文件对象，是切片类型。\n最后遍历所有文件进行保存处理即可。\n注意：该方法同样适用于不同的表单键，不过需要进行遍历保存。\n设置处理多部分表单请求的最大内存大小 MaxMultipartMemory 是 *gin.Engine 对象的一个字段，表示Gin框架处理multipart/form-data类型请求时，可以在内存中存储的最大数据大小。\n当一个multipart/form-data请求中的数据大小超过了MaxMultipartMemory限制时，Gin框架将会自动将超出部分的数据写入到磁盘的临时文件中，而不是全部存储在内存中。这是为了防止占用过多内存导致内存溢出的问题。例如，在文件上传的情况下，较大的文件将不会全部加载到内存中，而是部分写入临时文件，从而减少内存消耗。\nGin默认处理多部分表单请求的最大内存是32MiB。\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) func main() { r := gin.Default() // 设置最大上传文件大小为8MB r.MaxMultipartMemory = 8 \u0026lt;\u0026lt; 20 // 8 MiB r.POST(\u0026#34;/upload\u0026#34;, func(c *gin.Context) { file, err := c.FormFile(\u0026#34;file\u0026#34;) if err != nil { c.String(http.StatusBadRequest, \u0026#34;Bad request\u0026#34;) return } err = c.SaveUploadedFile(file, \u0026#34;./uploads/\u0026#34; + file.Filename) if err != nil { c.String(http.StatusInternalServerError, \u0026#34;Could not save file\u0026#34;) return } c.String(http.StatusOK, fmt.Sprintf(\u0026#34;File %s uploaded successfully.\u0026#34;, file.Filename)) }) r.Run(\u0026#34;:8080\u0026#34;) } 在这个例子中，r.MaxMultipartMemory = 8 \u0026lt;\u0026lt; 20 将Gin框架处理multipart/form-data类型请求的最大内存大小设置为8MiB。\n8 \u0026lt;\u0026lt; 20：这个表达式利用左移运算符将8左移20位，等价于8 * 2^20，即8 MiB（兆字节）。这种写法简洁明了，容易理解和记忆。\nGin会将超过部分的数据写入到磁盘的临时文件中，而不是全部存储在内存中。这样可以有效防止大文件上传时内存占用过多的问题。\n单位扩展\nMiB和MB都读兆字节。 二中的区别是使用的计数法不同。 MiB使用二进制计数法。1MiB = 1024KiB = 1,048,576 字节 = 2^20 字节 MB使用十进制计数法。1MB = 1000KB = 1000000 字节 = 10^6 字节 通常现实生活中使用的是MB，计算机中使用的是MiB。相同数值下，MiB表示的空间更大。 如购买的一块机械硬盘为2T，在计算机中显示通常小于2T。因为操作系统使用的是二进制计算方法。 使用中间件处理文件上传 通常我们会使用中间件来处理文件上传，比如验证文件类型、限制文件大小等。\n限制文件大小中间件：\n1 2 3 4 5 6 7 8 9 10 11 func MaxSizeAllowedMiddleware(n int64) gin.HandlerFunc { return func(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, n) if err := c.Request.ParseMultipartForm(n); err != nil { c.String(http.StatusRequestEntityTooLarge, \u0026#34;文件太大了, 不能超过%d兆字节: %s\u0026#34;, n\u0026gt;\u0026gt;20, err) c.Abort() return } c.Next() } } 验证文件类型中间件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func ValidateFileTypeMiddleware(allowExtMap map[string]bool) gin.HandlerFunc { return func(c *gin.Context) { file, err := c.FormFile(\u0026#34;file\u0026#34;) if err != nil { c.String(http.StatusBadRequest, \u0026#34;Bad Request: %s\u0026#34;, err) c.Abort() return } ext := path.Ext(file.Filename) if _, f := allowExtMap[ext]; !f { c.String(http.StatusBadRequest, \u0026#34;%q file type is not allowed\u0026#34;, ext) c.Abort() return } c.Next() } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 r.POST(\u0026#34;/upload\u0026#34;, MaxSizeAllowedMiddleware(3\u0026lt;\u0026lt;20), ValidateFileTypeMiddleware(map[string]bool{\u0026#34;.jpg\u0026#34;: true, \u0026#34;.png\u0026#34;: true}), func(c *gin.Context) { file, err := c.FormFile(\u0026#34;file\u0026#34;) if err != nil { c.String(http.StatusBadRequest, \u0026#34;Bad request: %s\u0026#34;, err) return } err = c.SaveUploadedFile(file, path.Join(\u0026#34;./uploads\u0026#34;, file.Filename)) if err != nil { c.String(http.StatusInternalServerError, \u0026#34;Could not save file: %s\u0026#34;, err) return } c.String(http.StatusOK, \u0026#34;File %s uploaded successfully.\u0026#34;, file.Filename) }) 注意事项：\n上面简单演示了单文件上传时，使用中间件限制上传文件的大小，以及类型。 如果是多文件上传，文件大小中间件可以适用，但文件类型中间件不适用，需要使用MultipartForm获取多部分表单请求对象，然后通过File字段遍历，判断文件类型。 他们的注册顺序： 限制文件大小的中间件： 这个中间件应该先执行，因为它可以尽早地检查请求体的大小，避免浪费资源去处理超过限制的请求。 验证文件类型的中间件： 在文件大小被验证通过之后，再进行文件类型的验证。这是因为文件类型的验证通常需要解析文件内容或元数据，属于较重的操作。 通过这种顺序设置，可以确保请求被有效地过滤，并避免不必要的资源消耗。 最后需要注意：如果先验证了文件类型，那么会消耗请求体内容大小，造成限制文件大小中间件失效。所以要先注册限制文件大小中间件。 接口访问推荐在接口测试工具中访问，如apipost。 使用Cookie Cookie介绍 Cookie 是在 HTTP 协议中用来存储少量数据的技术，由服务器发送到浏览器并存储在客户端。其主要作用包括：\n会话管理（Session Management）\nCookie 常用于会话管理，即在用户浏览网页期间保持用户的登录状态。常见的应用场景有：\n用户认证： 记录用户的登录信息，使得用户在浏览网站的不同页面时无需重新登录。 购物车： 在线商店使用 Cookie 来记录用户的购物车内容。 个性化（Personalization）\nCookie 可以用来记录用户的偏好设置，以便在用户访问网站时提供个性化的内容和体验。例如：\n用户界面设置： 保存用户选择的主题、语言等偏好设置。 个性化广告： 根据用户的浏览历史记录和偏好，展示相关的广告内容。 跟踪和分析（Tracking and Analytics）\nCookie 被广泛用于用户行为跟踪和数据分析，以帮助网站管理员了解用户如何使用他们的网站，并进行相应的优化。例如：\n流量分析： 记录用户访问的页面、停留时间等信息，用于流量统计和分析。 广告跟踪： 跟踪用户点击广告的情况，以评估广告效果。 安全性（Security）\n虽然 Cookie 本身不是安全机制，但它们可以与安全机制结合使用。例如：\n防止跨站请求伪造（CSRF）： 通过设置 HttpOnly 和 Secure 属性，增强 Cookie 的安全性，防止攻击者利用 JavaScript 窃取 Cookie 内容。 令牌存储： 将安全令牌存储在 Cookie 中，以便在每次请求时进行验证。 需要注意的是别使用cookie保存隐私数据。\n状态管理（State Management）\nHTTP 是无状态协议，服务器无法记录每次请求的状态。Cookie 可以帮助服务器存储和管理一些状态信息，例如：\n上次访问时间： 记录用户上次访问的时间，以便在用户再次访问时提供相关信息。 表单数据： 存储用户在表单中输入的数据，方便在用户返回时自动填充。 Cookie的属性 Cookie 的属性决定了它的行为和存储方式，常见的属性包括：\nName 和 Value： Cookie 的名称和值。\nDomain： 指定 Cookie 所属的域，只有该域及其子域可以访问 Cookie。\nPath： 指定 Cookie 所属的路径，只有该路径及其子路径可以访问 Cookie。\nMax-Age/Expires： 指定 Cookie 的有效期，超过这个时间后，Cookie 将被删除。\nSecure： 指定 Cookie 仅在 HTTPS 连接中发送。\nHttpOnly： 指定 Cookie 不能通过 JavaScript 访问，增强安全性。\nSameSite： 防止跨站请求伪造（CSRF）攻击，有三个值可以选择：Strict、Lax 和 None。\n补充 Cookie本质就是一个请求头，可以在请求头中查看Cookie。 浏览器设置Cookie是通过响应头Set-Cookie设置的。 查看Cookie还可以再浏览器的开发者工具的Application\u0026ndash;\u0026gt;Storage中查看对应域名下的Cookie。 不同浏览器可能有所不同。 在Gin中使用Cookie 设置Cookie\n在Gin中，使用SetCookie方法可以在响应中设置一个cookie：\n1 func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) 参数说明：\nname: cookie的名称。\nvalue: cookie的值。\nmaxAge: cookie的最大存活时间（以秒为单位）。\n有效时间，单位是秒。 MaxAge=0，忽略MaxAge属性，就是忽略存活时间，有效期在浏览器会话结束时删除。 MaxAge\u0026lt;0 相当于删除cookie, 通常可以设置-1代表删除。 MaxAge\u0026gt;0 多少秒后cookie失效。 path: cookie的路径。通常设置为\u0026quot;/\u0026ldquo;表示整个站点。\ndomain: cookie的域。本地调试配置成 localhost , 正式上线配置成域名。\n子域共享Cookie：\n在域名前面加点表示：该域名及其子域共享Cookie。没有只能该域访问设置的Cookie。子域无法访问。如：\n1 2 // 设置 Cookie，域为 .example.com，子域名可以共享 c.SetCookie(\u0026#34;username\u0026#34;, \u0026#34;gin_user\u0026#34;, 3600, \u0026#34;/\u0026#34;, \u0026#34;.example.com\u0026#34;, false, true) 在 SetCookie 方法中，将域设置为 .example.com。这使得所有 example.com 及其子域（如 sub1.example.com 和 sub2.example.com）都可以访问该 Cookie。\n注意：跨不同的顶级域名、二级域名无法共享Cookie。（如 example.com 和 anotherexample.com）\n浏览器支持：确保浏览器支持跨子域名的 Cookie 设置，现代浏览器一般都支持。\nsecure: 是否仅在HTTPS请求中发送cookie。设置为false表示在HTTP和HTTPS请求中都发送。\n在生产环境中，建议将 Secure 设置为 true，以确保 Cookie 仅在 HTTPS 请求中发送。 httpOnly: 是否将cookie标记为HttpOnly。设置为true表示客户端JavaScript无法访问cookie。\n获取Cookie\n使用Cookie方法可以从请求中获取cookie：\n1 func (c *Context) Cookie(name string) (string, error) Cookie方法用于获取指定Cookie键的值，如果获取成功，将返回cookie的值，否则返回错误。\n删除Cookie\n在Gin中，删除cookie实际上是通过设置一个过期的cookie来实现的。\n只需要将要删除的cookie的存活时间maxAge参数设置为-1，这表示将cookie立即过期，从而达到删除cookie的效果。\n1 func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) 示例：\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 package main import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34; ) func main() { r := gin.Default() r.GET(\u0026#34;/set_cookie\u0026#34;, func(c *gin.Context) { c.SetCookie(\u0026#34;username\u0026#34;, \u0026#34;gin_user\u0026#34;, 3600, \u0026#34;/\u0026#34;, \u0026#34;localhost\u0026#34;, false, true) c.JSON(http.StatusOK, gin.H{ \u0026#34;message\u0026#34;: \u0026#34;Cookie set successfully!\u0026#34;, }) }) r.GET(\u0026#34;/get_cookie\u0026#34;, func(c *gin.Context) { cookie, err := c.Cookie(\u0026#34;username\u0026#34;) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ \u0026#34;message\u0026#34;: \u0026#34;Cookie not found! \u0026#34; + err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ \u0026#34;username\u0026#34;: cookie, }) }) r.GET(\u0026#34;/delete_cookie\u0026#34;, func(c *gin.Context) { c.SetCookie(\u0026#34;username\u0026#34;, \u0026#34;\u0026#34;, -1, \u0026#34;/\u0026#34;, \u0026#34;localhost\u0026#34;, false, true) c.JSON(http.StatusOK, gin.H{ \u0026#34;message\u0026#34;: \u0026#34;Cookie deleted successfully!\u0026#34;, }) }) r.Run(\u0026#34;:8080\u0026#34;) } 使用Session Session介绍 简单介绍\nSession是另一种存储少量数据的技术，不同的是Cookie保存在客户端浏览器中，而session保存在服务器上。\n作用\nSession是一种在客户端和服务器之间维持用户会话状态的机制。Session用于在多个请求之间保持用户的状态和信息，这在需要用户认证和个性化服务时非常有用。它可以存储用户的登录状态、购物车信息、用户偏好等。\n属性\nSession的属性跟Cookie的属性类似。参考Cookie的属性。\nSession常用属性的默认值（Cookie需要没有）：\n1 2 3 4 5 Path: \u0026#34;/\u0026#34;, Domain: \u0026#34;localhost\u0026#34;, MaxAge: 一个月, HttpOnly: false, Secure: true, 键跟值通过内置方法管理。\n工作流程\n当客户端（浏览器）第一次访问需要Session的页面时，服务器端会创建一个session对象，拥有唯一的一个Session ID，数据就保存在类似于key,value的键值对，然后将Session对象保存到服务器设置的存储引擎中，最后将Session ID返回到浏览器(客户)端。浏览器下次访问时会携带Session ID(cookie)，找到对应的Session对象。\n在后续的请求中，客户端会携带之前收到的Session Cookie，包含唯一的Session ID。\n服务器接收到请求时，通过Session ID从存储引擎中查找对应的Session数据（键值对）。\n服务器负责Session的创建、更新和销毁。Session通常有一个生命周期，在一段时间不活动后会自动过期。\n因为Gin本身没有内置的Session管理功能，所以在Gin中使用Session需要配合第三方库（中间件）实现，比较受欢迎的是gin-contrib/sessions。\n在Gin中使用Session 要在Gin框架中使用Session，需要借助一些中间件库实现，例如github.com/gin-contrib/sessions。\ngin-contrib/sessions中间件支持的存储引擎：\ncookie-based Redis memcached MongoDB GORM memstore PostgreSQL 安装依赖：\n1 go get -u github.com/gin-contrib/sessions 下面介绍常用的存储引擎。\n基于Cookie存储Session 基于Cookie存储Session是将Session中的数据存储在Cookie当中（与Session ID一同存放）。使用示例如下：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package main import ( \u0026#34;github.com/gin-contrib/sessions\u0026#34; \u0026#34;github.com/gin-contrib/sessions/cookie\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) func main() { r := gin.Default() // 使用cookie-based存储 store := cookie.NewStore([]byte(\u0026#34;secret\u0026#34;)) r.Use(sessions.Sessions(\u0026#34;mysession\u0026#34;, store)) r.GET(\u0026#34;/login\u0026#34;, loginHandler) r.GET(\u0026#34;/logout\u0026#34;, logoutHandler) r.GET(\u0026#34;/profile\u0026#34;, authRequired(), profileHandler) r.Run(\u0026#34;:8080\u0026#34;) } // 登录处理 func loginHandler(c *gin.Context) { session := sessions.Default(c) uId := c.Query(\u0026#34;id\u0026#34;) password := c.Query(\u0026#34;password\u0026#34;) // 模拟登录认证 if password == \u0026#34;123456\u0026#34; { session.Set(\u0026#34;user\u0026#34;, uId) session.Options(sessions.Options{ MaxAge: 3600, // 一小时过期 HttpOnly: true, }) _ = session.Save() c.JSON(http.StatusOK, gin.H{\u0026#34;message\u0026#34;: \u0026#34;logged in\u0026#34;}) } else { c.JSON(http.StatusUnauthorized, gin.H{\u0026#34;message\u0026#34;: \u0026#34;login failed\u0026#34;}) } } // 注销处理 func logoutHandler(c *gin.Context) { session := sessions.Default(c) session.Delete(\u0026#34;user\u0026#34;) session.Save() c.JSON(200, gin.H{\u0026#34;message\u0026#34;: \u0026#34;logged out\u0026#34;}) } // 需要认证的处理 func authRequired() gin.HandlerFunc { return func(c *gin.Context) { session := sessions.Default(c) uId := session.Get(\u0026#34;user\u0026#34;) if uId == nil { c.JSON(401, gin.H{\u0026#34;message\u0026#34;: \u0026#34;unauthorized\u0026#34;}) c.Abort() return } c.Next() } } // 用户信息处理 func profileHandler(c *gin.Context) { session := sessions.Default(c) uId := session.Get(\u0026#34;user\u0026#34;) c.JSON(200, gin.H{\u0026#34;user\u0026#34;: uId}) } 解释各部分功能：\n创建Session存储引擎：使用cookie.NewStore创建一个基于Cookie的Session存储引擎。参数是一个字节切片，是Session的加密秘钥。\n注册Session中间件：使用sessions.Sessions(\u0026quot;mysession\u0026quot;, store)注册一个Session中间件，它接收两个参数，第一个是Session ID的键（string类型），第二个是存储引擎。\n如果要注册多个Session中间件，使用sessions.SessionsMany(sessionNames, store)，它接收两个参数，第一个不同Session ID的键（string切片类型），第二个是存储引擎。一个存储引擎可以存储多个Session。 获取Session对象：使用sessions.Default(c)获取Session对象，这个方法是获取只注册一个Session中间件的快捷方式。参数是Gin上下文对象。\n如果注册的是多个Session中间件，获取不同的Session对象，使用sessions.DefaultMany(c, \u0026quot;a\u0026quot;)，这个方法是获取注册多个Session中间件的快捷方式，第一个参数是Gin上下文对象，第二个是要获取Session对象的Session ID的键。 1 2 3 4 5 6 7 8 9 store := cookie.NewStore([]byte(\u0026#34;secret\u0026#34;)) sessionNames := []string{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;} r.Use(sessions.SessionsMany(sessionNames, store)) r.GET(\u0026#34;/hello\u0026#34;, func(c *gin.Context) { sessionA := sessions.DefaultMany(c, \u0026#34;a\u0026#34;) sessionB := sessions.DefaultMany(c, \u0026#34;b\u0026#34;) ... } 修改Session对象属性：使用Options(sessions.Options{})方法可以修改Session的默认属性。\n设置Session数据：使用Set方法设置Session数据（key，value键值对）。可以重复添加数据。\n删除Session数据：使用Delete方法删除Session数据。参数是数据的键。\n理解：服务器接收到请求之后会从Cookie中自动获取Session ID，调用Default方法会根据Session ID获取对应的Session对象。数据就保存在Session对象中。因为Session ID是唯一的，所以获取的数据就是本次请求中的，可以通过数据的键进行删除。（不同用户的Session是独立的，相互之间不会干扰。） 保存Session数据：使用Save方法保存Session数据。注意每次修改过Session数据之后，都需要调用此方法，数据才能生效。\n删除当前Session ID所有的Session数据：使用Clear方法即可删除当前Session对象所有的Session数据。\n基于Redis存储Session 基于Redis存储Session就是将Session数据保存在Redis中，当浏览器发送请求时，服务器会根据Cookie中的Session ID，获取对应的Session对象。\n学会了基于Cookie存储Session，那么基于Redis存储Session就很简单。我们只需要将存储引擎修改为Redis即可。其他使用方法一模一样。\n创建Cookie存储引擎使用的是cookie.NewStore([]byte(\u0026quot;secret\u0026quot;))方法。\n创建Reds存储引擎使用的是redis.NewStore(10, \u0026quot;tcp\u0026quot;, \u0026quot;localhost:6379\u0026quot;, \u0026quot;\u0026quot;, []byte(\u0026quot;secret\u0026quot;))方法，参数介绍：\n第一个：Redis最大的空闲连接数。 第二个：数通信协议tcp或者udp。连接类型。 第三个：Redis服务器地址，格式：host:port。 第四个：Redis密码。 最后一个：Session加密秘钥。 默认连接0号库。需要指定几号Redis数据库，使用redis.NewStoreWithDB(10, \u0026quot;tcp\u0026quot;, \u0026quot;192.168.245.130:6379\u0026quot;, \u0026quot;123456\u0026quot;, \u0026quot;1\u0026quot;, []byte(\u0026quot;secret\u0026quot;))方法。\nSessions加密秘钥介绍 加密密钥的作用\n数据加密： 因为Session的部分数据（Session ID），会保存在Cookie中响应给客户端，加密密钥用于加密这些数据，以防止客户端篡改。 加密后的数据是不可读的，只有持有加密密钥的服务器才能解密和读取这些数据。 数据签名： 除了加密，加密密钥也用于对数据进行签名，以确保数据的完整性和真实性。 签名确保数据在传输过程中未被篡改，如果数据被篡改，签名验证将失败。 使用加密秘钥\n在创建存储引擎的时候传入加密密钥。\n加密密钥是一个字节切片，可以通过一个字符串转换而来。\n加密秘钥是可选参数，但是必须要有一个身份验证秘钥，Sessions中间件才能起作用。\n当只有一个加密秘钥时，用于身份验证和加密。\n当存在多加密秘钥时，第一个秘钥用于身份验证，后续秘钥用于加密。\n建议使用 32 或 64 字节的身份验证密钥。\n如果设置了加密密钥，则必须为 16、24 或 32 字节，以选择 AES-128、AES-192 或 AES-256 模式。否则Sessions中间件不起作用。\n常见的情况是设置单个身份验证密钥和可选的加密密钥。\n示例：\n1 2 3 redis.NewStoreWithDB(10, \u0026#34;tcp\u0026#34;, \u0026#34;192.168.245.130:6379\u0026#34;, \u0026#34;123456\u0026#34;, \u0026#34;1\u0026#34;, []byte(\u0026#34;secret\u0026#34;), []byte(\u0026#34;1234567890123456\u0026#34;)) 至此：\n设置 Session 数据时，数据会使用加密密钥加密后存储在 Cookie 中。 获取 Session 数据时，数据会使用加密密钥解密。 注意事项\n密钥长度：使用足够长和随机的密钥来增强安全性，避免使用过短或容易猜测的密钥。 密钥管理：妥善管理加密密钥，避免泄露。可以使用环境变量或专门的密钥管理服务来存储和读取密钥。 HTTPS：在生产环境中，确保使用 HTTPS 来加密传输中的数据，防止中间人攻击。 总结 本文简单介绍了Gin框架的基本使用。\n参考 Go gin框架入门教程 ","date":"2024-05-21T17:58:00+08:00","permalink":"https://arlettebrook.github.io/p/gin-introduction/","title":"Gin Introduction"},{"content":" Go代码约定 Go代码约定是@unknwon为 Go 编程语言制定的 100% 固执己见和偏执的代码约定。它可能与Go 代码审查注释或任何其他指南兼容，也可能不兼容。\n总之，以下介绍的的约定，在大部分Go开源项目中都遵循。\n版权声明 作为开源项目，必须有相应的开源许可证才能算是真正的开源。在选择了一个开源许可证之后，需要在源文件中进行相应的版权声明才能生效。以下分别以 Apache License, Version 2.0 和 MIT 授权许可为例。\nApache License, Version 2.0 该许可证要求在所有的源文件中的头部放置以下内容才能算协议对该文件有效：\n1 2 3 4 5 6 7 8 9 10 11 12 13 // Copyright [yyyy] [name of copyright owner] [\u0026lt;联系方式\u0026gt;] // // Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. 其中，[yyyy] 表示该源文件创建的年份。紧随其后的是 [name of copyright owner]，即版权所有者。如果为个人项目，就写个人名称；若为团队项目，则宜写团队名称。[\u0026lt;联系方式\u0026gt;]：联系方式，通常为邮箱。\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 // Copyright 2013-2023 The Cobra Authors // // Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. MIT License 一般使用 MIT 授权的项目，需在源文件头部增加以下内容：\n1 2 3 // Copyright [yyyy] [name of copyright owner] [\u0026lt;联系方式\u0026gt;]. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. 其中，年份、版权所有者的名称以及联系方式填写规则与 Apache License, Version 2.0 的一样。\n示例:\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 /* Copyright © 2024 arlettebrook \u0026lt;arlettebrook@proton.me\u0026gt; Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \u0026#34;Software\u0026#34;), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED \u0026#34;AS IS\u0026#34;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;Hello, world!\u0026#34;) } 其它说明 其它类型的开源许可证基本上都可参照以上两种方案。\n如果存在不同作者或组织对同个源文件的修改，在协议兼容的情况下，可将首行变为多行，按照先后次序排放：\n1 2 // Copyright 2011 Gary Burd // Copyright 2013 Unknwon 在 README 文件最后中需要说明项目所采用的开源许可证：\n1 2 3 ## 授权许可 本项目采用 MIT 开源授权许可证，完整的授权说明已放置在 [LICENSE](LICENSE) 文件中。 开源协议介绍参考.\n项目结构 以下为一般项目结构，根据不同的 Web 框架习惯，可使用括号内的文字替换；根据不同的项目类型和需求，可自由增删某些结构：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 - templates (views) # 模板文件 - public (static) # 静态文件 - css - fonts - img - js - routes # 路由逻辑处理 - models\t# 数据逻辑层 - pkg # 子模块 - setting # 应用配置存取 - cmd # 命令行程序命令 - conf # 默认配置 - locale # i18n 本地化文件 - custom # 自定义配置 - data # 应用生成数据文件 - log # 应用生成日志文件 命令行应用 当应用类型为命令行应用时，需要将命令相关文件存放于 /cmd 目录下，并为每个命令创建一个单独的源文件：\n1 2 3 4 5 6 /cmd dump.go fix.go serve.go update.go web.go 导入标准库、第三方或其它包 除标准库外，Go 语言的导入路径基本上依赖代码托管平台上的 URL 路径，因此一个源文件需要导入的包有 4 种分类：标准库、第三方包、组织内其它包和当前包的子包。\n基本规则：\n如果同时存在 2 种及以上，则需要使用分组来导入。每个分类使用一个分组，采用空行作为分区之间的分割。 在非测试文件（*_test.go）中，禁止使用 . 来简化导入包的对象调用。 禁止使用相对路径导入（./subpackage），所有导入路径必须符合 go get 标准。 下面是一个完整的示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import ( \u0026#34;fmt\u0026#34; \u0026#34;html/template\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/codegangsta/cli\u0026#34; \u0026#34;gopkg.in/macaron.v1\u0026#34; \u0026#34;github.com/gogits/git\u0026#34; \u0026#34;github.com/gogits/gfm\u0026#34; \u0026#34;github.com/gogits/gogs/routers\u0026#34; \u0026#34;github.com/gogits/gogs/routers/repo\u0026#34; \u0026#34;github.com/gogits/gogs/routers/user\u0026#34; ) 通常我们都不需要关注，包是如何分组的。只需要运行goimports命令，即可自动导入，自动分组。\n在许多ide中，建议不要使用自带的导入。它们分组不是很准确，建议使用goimports。如：在GoLand中，我们只需要在File Watchers中勾选goimports命令，即可实现自动导入、分组。\n注释规范 所有导出对象都需要注释说明其用途；非导出对象根据情况进行注释。 如果对象可数且无明确指定数量的情况下，一律使用单数形式和一般进行时描述；否则使用复数形式。 包、函数、方法和类型的注释说明都是一个完整的句子。 句子类型的注释首字母均需大写；短语类型的注释首字母需小写。 注释的单行长度不能超过 80 个字符。 包级别 包级别的注释就是对包的介绍，只需在同个包的任一源文件中说明即可有效。\n对于 main 包，一般只有一行简短的注释用以说明包的用途，且以项目名称开头：\n1 2 // Gogs (Go Git Service) is a painless self-hosted Git Service. package main 对于一个复杂项目的子包，一般情况下不需要包级别注释，除非是代表某个特定功能的模块。\n对于简单的非 main 包，也可用一行注释概括。\n对于相对功能复杂的非 main 包，一般都会增加一些使用示例或基本说明，且以 Package \u0026lt;name\u0026gt; 开头：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /* Package regexp implements a simple library for regular expressions. The syntax of the regular expressions accepted is: regexp: concatenation { \u0026#39;|\u0026#39; concatenation } concatenation: { closure } closure: term [ \u0026#39;*\u0026#39; | \u0026#39;+\u0026#39; | \u0026#39;?\u0026#39; ] term: \u0026#39;^\u0026#39; \u0026#39;$\u0026#39; \u0026#39;.\u0026#39; character \u0026#39;[\u0026#39; [ \u0026#39;^\u0026#39; ] character-ranges \u0026#39;]\u0026#39; \u0026#39;(\u0026#39; regexp \u0026#39;)\u0026#39; */ package regexp 特别复杂的包说明，可单独创建 文件来加以说明。\ndoc.go专门书写该go文件所在包的文档介绍。 结构、接口及其它类型 类型的定义一般都以单数形式描述：\n1 2 // Request represents a request to run a command. type Request struct { ... 如果为接口，则一般以以下形式描述：\n1 2 // FileInfo is the interface that describes a file and is returned by Stat and Lstat. type FileInfo interface { ... 函数与方法 函数与方法的注释需以函数或方法的名称作为开头：\n1 // Post returns *BeegoHttpRequest with POST method. 如果一句话不足以说明全部问题，则可换行继续进行更加细致的描述：\n1 2 // Copy copies file from source to target path. // It returns false and error when error occurs in underlying function calls. 若函数或方法为判断类型（返回值主要为 bool 类型），则以 \u0026lt;name\u0026gt; returns true if 开头：\n1 2 // HasPrefix returns true if name has any string in given slice as prefix. func HasPrefix(name string, prefixes []string) bool { ... 其它说明 当某个部分等待完成时，可用 TODO: 开头的注释来提醒维护人员。\n当某个部分存在已知问题进行需要修复或改进时，可用 FIXME: 开头的注释来提醒维护人员。\n当需要特别说明某个问题时，可用 NOTE: 开头的注释：\n1 2 3 // NOTE: os.Chmod and os.Chtimes don\u0026#39;t recognize symbolic link, // which will lead \u0026#34;no such file or directory\u0026#34; error. return os.Symlink(target, dest) 注意：NOTE:部分ide不支持高亮。\n命名规则 文件名 整个应用或包的主入口文件应当是 main.go 或与应用名称简写相同。例如：Gogs 的主入口文件名为 gogs.go。 函数或方法 若函数或方法为判断类型（返回值主要为 bool 类型），则名称应以 Has, Is, Can 或 Allow 等判断性动词开头：\n1 2 3 4 func HasPrefix(name string, prefixes []string) bool { ... } func IsEntry(name string, entries []string) bool { ... } func CanManage(name string) bool { ... } func AllowGitHook() bool { ... } 常量 常量均需使用全部大写字母组成，并使用下划线分词：\n1 const APP_VER = \u0026#34;0.7.0.1110 Beta\u0026#34; 如果是枚举类型的常量，需要先创建相应类型：\n1 2 3 4 5 6 type Scheme string const ( HTTP Scheme = \u0026#34;http\u0026#34; HTTPS Scheme = \u0026#34;https\u0026#34; ) 如果模块的功能较为复杂、常量名称容易混淆的情况下，为了更好地区分枚举类型，可以使用完整的前缀：\n1 2 3 4 5 6 7 type PullRequestStatus int const ( PULL_REQUEST_STATUS_CONFLICT PullRequestStatus = iota PULL_REQUEST_STATUS_CHECKING PULL_REQUEST_STATUS_MERGEABLE ) 变量 变量命名基本上遵循相应的英文表达或简写。\n在相对简单的环境（对象数量少、针对性强）中，可以将一些名称由完整单词简写为单个字母，例如：\nuser 可以简写为 u userID 可以简写 uid 若变量类型为 bool 类型，则名称应以 Has, Is, Can 或 Allow 开头：\n1 2 3 4 var isExist bool var hasConflict bool var canManage bool var allowGitHook bool 上面的规则也适用于结构体字段定义：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // Webhook represents a web hook object. type Webhook struct { ID int64 `xorm:\u0026#34;pk autoincr\u0026#34;` RepoID int64 OrgID int64 URL string `xorm:\u0026#34;url TEXT\u0026#34;` ContentType HookContentType Secret string `xorm:\u0026#34;TEXT\u0026#34;` Events string `xorm:\u0026#34;TEXT\u0026#34;` *HookEvent `xorm:\u0026#34;-\u0026#34;` IsSSL bool `xorm:\u0026#34;is_ssl\u0026#34;` IsActive bool HookTaskType HookTaskType Meta string `xorm:\u0026#34;TEXT\u0026#34;` // store hook-specific attributes LastStatus HookStatus // Last delivery status Created time.Time `xorm:\u0026#34;CREATED\u0026#34;` Updated time.Time `xorm:\u0026#34;UPDATED\u0026#34;` } 变量命名惯例 变量名称一般遵循驼峰法，但遇到特有名词时，需要遵循以下规则：\n如果变量为私有，且特有名词为首个单词，则使用小写，如 apiClient。 其它情况都应当使用该名词原有的写法，如 APIClient、GET、UserID。 下面列举了一些常见的特有名词：\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 // A GonicMapper that contains a list of common initialisms taken from golang/lint var LintGonicMapper = GonicMapper{ \u0026#34;API\u0026#34;: true, \u0026#34;ASCII\u0026#34;: true, \u0026#34;CPU\u0026#34;: true, \u0026#34;CSS\u0026#34;: true, \u0026#34;DNS\u0026#34;: true, \u0026#34;EOF\u0026#34;: true, \u0026#34;GUID\u0026#34;: true, \u0026#34;HTML\u0026#34;: true, \u0026#34;HTTP\u0026#34;: true, \u0026#34;HTTPS\u0026#34;: true, \u0026#34;ID\u0026#34;: true, \u0026#34;IP\u0026#34;: true, \u0026#34;JSON\u0026#34;: true, \u0026#34;LHS\u0026#34;: true, \u0026#34;QPS\u0026#34;: true, \u0026#34;RAM\u0026#34;: true, \u0026#34;RHS\u0026#34;: true, \u0026#34;RPC\u0026#34;: true, \u0026#34;SLA\u0026#34;: true, \u0026#34;SMTP\u0026#34;: true, \u0026#34;SSH\u0026#34;: true, \u0026#34;TLS\u0026#34;: true, \u0026#34;TTL\u0026#34;: true, \u0026#34;UI\u0026#34;: true, \u0026#34;UID\u0026#34;: true, \u0026#34;UUID\u0026#34;: true, \u0026#34;URI\u0026#34;: true, \u0026#34;URL\u0026#34;: true, \u0026#34;UTF8\u0026#34;: true, \u0026#34;VM\u0026#34;: true, \u0026#34;XML\u0026#34;: true, \u0026#34;XSRF\u0026#34;: true, \u0026#34;XSS\u0026#34;: true, } 声明语句 函数或方法 函数或方法的参数排列顺序遵循以下几点原则（从左到右）：\n参数的重要程度与逻辑顺序 简单类型优先于复杂类型 尽可能将同种类型的参数放在相邻位置，则只需写一次类型 示例:\n以下声明语句，User 类型要复杂于 string 类型，但由于 Repository 是 User 的附属品，首先确定 User 才能继而确定 Repository。因此，User 的顺序要优先于 repoName。\n1 func IsRepositoryExist(user *User, repoName string) (bool, error) { ... ​\n代码指导 基本约束 所有应用的 main 包需要有 APP_VER 常量表示版本，格式为 X.Y.Z.Date [Status]，例如：0.7.6.1112 Beta。\n单独的库需要有函数 Version 返回库版本号的字符串，格式为 X.Y.Z[.Date]。\n当单行代码超过 80 个字符时，就要考虑分行。分行的规则是以参数为单位将从较长的参数开始换行，以此类推直到每行长度合适：\n1 2 3 So(z.ExtractTo( path.Join(os.TempDir(), \u0026#34;testdata/test2\u0026#34;), \u0026#34;dir/\u0026#34;, \u0026#34;dir/bar\u0026#34;, \u0026#34;readonly\u0026#34;), ShouldBeNil) 当单行声明语句超过 80 个字符时，就要考虑分行。分行的规则是将参数按类型分组，紧接着的声明语句的是一个空行，以便和函数体区别：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // NewNode initializes and returns a new Node representation. func NewNode( importPath, downloadUrl string, tp RevisionType, val string, isGetDeps bool) *Node { n := \u0026amp;Node{ Pkg: Pkg{ ImportPath: importPath, RootPath: GetRootPath(importPath), Type: tp, Value: val, }, DownloadURL: downloadUrl, IsGetDeps: isGetDeps, } n.InstallPath = path.Join(setting.InstallRepoPath, n.RootPath) + n.ValSuffix() return n } 分组声明一般需要按照功能来区分，而不是将所有类型都分在一组：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const ( // Default section name. DEFAULT_SECTION = \u0026#34;DEFAULT\u0026#34; // Maximum allowed depth when recursively substituing variable names. _DEPTH_VALUES = 200 ) type ParseError int const ( ERR_SECTION_NOT_FOUND ParseError = iota + 1 ERR_KEY_NOT_FOUND ERR_BLANK_SECTION_NAME ERR_COULD_NOT_PARSE ) 当一个源文件中存在多个相对独立的部分时，为方便区分，需使用由 ASCII Generator 提供的句型字符标注（示例：Comment）：\n1 2 3 4 5 6 // _________ __ // \\_ ___ \\ ____ _____ _____ ____ _____/ |_ // / \\ \\/ / _ \\ / \\ / \\_/ __ \\ / \\ __\\ // \\ \\___( \u0026lt;_\u0026gt; ) Y Y \\ Y Y \\ ___/| | \\ | // \\______ /\\____/|__|_| /__|_| /\\___ \u0026gt;___| /__| // \\/ \\/ \\/ \\/ \\/ 函数或方法的顺序一般需要按照依赖关系由浅入深由上至下排序，即最底层的函数出现在最前面。例如，下方的代码，函数 ExecCmdDirBytes 属于最底层的函数，它被 ExecCmdDir 函数调用，而 ExecCmdDir 又被 ExecCmd 调用：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // ExecCmdDirBytes executes system command in given directory // and return stdout, stderr in bytes type, along with possible error. func ExecCmdDirBytes(dir, cmdName string, args ...string) ([]byte, []byte, error) { ... } // ExecCmdDir executes system command in given directory // and return stdout, stderr in string type, along with possible error. func ExecCmdDir(dir, cmdName string, args ...string) (string, string, error) { bufOut, bufErr, err := ExecCmdDirBytes(dir, cmdName, args...) return string(bufOut), string(bufErr), err } // ExecCmd executes system command // and return stdout, stderr in string type, along with possible error. func ExecCmd(cmdName string, args ...string) (string, string, error) { return ExecCmdDir(\u0026#34;\u0026#34;, cmdName, args...) } 结构附带的方法应置于结构定义之后，按照所对应操作的字段顺序摆放方法：\n1 2 3 4 type Webhook struct { ... } func (w *Webhook) GetEvent() { ... } func (w *Webhook) SaveEvent() error { ... } func (w *Webhook) HasPushEvent() bool { ... } 如果一个结构拥有对应操作函数，大体上按照 CRUD 的顺序放置结构定义之后：\n1 2 3 4 func CreateWebhook(w *Webhook) error { ... } func GetWebhookById(hookId int64) (*Webhook, error) { ... } func UpdateWebhook(w *Webhook) error { ... } func DeleteWebhook(hookId int64) error { ... } 如果一个结构拥有以 Has、Is、Can 或 Allow 开头的函数或方法，则应将它们至于所有其它函数及方法之前；这些函数或方法以 Has、Is、Can、Allow 的顺序排序。\n变量的定义要放置在相关函数之前：\n1 2 3 4 5 6 7 8 var CmdDump = cli.Command{ Name: \u0026#34;dump\u0026#34;, ... Action: runDump, Flags: []cli.Flag{}, } func runDump(*cli.Context) { ... 在初始化结构时，尽可能使用一一对应方式：\n1 2 3 4 5 6 7 AddHookTask(\u0026amp;HookTask{ Type: HTT_WEBHOOK, Url: w.Url, Payload: p, ContentType: w.ContentType, IsSsl: w.IsSsl, }) 测试用例 单元测试都必须使用 GoConvey 编写，且辅助包覆盖率必须在 80% 以上。 使用示例 为辅助包书写使用示例的时，文件名均命名为 example_test.go。 测试用例的函数名称必须以 Test_ 开头，例如：Test_Logger。 如果为方法书写测试用例，则需要以 Text_\u0026lt;Struct\u0026gt;_\u0026lt;Method\u0026gt; 的形式命名，例如：Test_Macaron_Run。 或者用驼峰命名。 更多go代码风格指南 Effective Go Go Common Mistakes Go Code Review Comments The Uber Go Style Guide 总结 本文简单介绍了在编写go程序时，应该遵循的一些风格、约定。尽管内容有很多，不过我们也不必着急，目前许多go的IDE都会帮我们检查这些规范。\n参考 原作者：go-code-convention ","date":"2024-05-21T14:48:21+08:00","permalink":"https://arlettebrook.github.io/p/go-code-style-guide-introduction/","title":"Go Code Style Guide Introduction"},{"content":" 简介 go-ini是 Go 语言中用于操作 ini 文件的第三方库。\n本文介绍go-ini库的使用。\nini配置文件介绍 ini 是 Windows 上常用的配置文件格式。MySQL 的 Windows 版就是使用 ini 格式存储配置的。\n.ini文件是Initialization File的缩写，即初始化文件，ini文件格式：[节/section/分区/表/段]+键=值。\n节可以为空，但参数（key=value）就需要写在开头。因为一个section没有明显的结束标识符，一个section的开始就是上一个section的结束，或者是文件结束。 所有的section名称都是独占一行，并且section名字都被方括号包围着（[和]）。 ini文件不支持多个方括号嵌套。有的就不以ini配置文件格式读取。 ini配置文件后缀不一定是.ini，也可以是.cfg、.conf或者是.txt。 节名区分大小写，建议用_连接。 所有的参数都是以section为单位结合在一起的。可以有多个参数，但一个参数独占一行。 在section声明后的所有parameters都属于该section。 区分大小写，建议用_连接。 注释（comments）使用分号表示（;）或者#号，在分号、#号后面的文字，直到该行结尾都全部为注释。\n1 2 3 4 5 # app=name app_name = awesome web [mysql] ip = 127.0.0.1 ; database=mysql .ini文件是windows的系统配置文件，统管windows的各项配置，最重要的就是“System.ini”、“System32.ini”和“Win.ini”。该文件主要存放用户所做的选择以及系统的各种参数。用户可以通过修改INI文件，来改变应用程序和系统的很多配置。一般用户就用windows提供的各项图形化管理界面就可实现相同的配置了，但在某些情况，还是要直接编辑.ini才方便，一般只有很熟悉windows才能去直接编辑。\n在Windows系统中，注册表的出现，让INI文件在Windows系统的地位就开始不断下滑，因为注册表独特优点，使应用程序和系统都把许多参数和初始化信息存放进了注册表中。但在某些场合，INI文件还拥有不可替代的地位。 在ini配置文件中，可以使用占位符%(name)s表示用之前已定义的键name的值来替换，这里的s表示值为字符串类型。4\n在section名称中可以用.来表示两个或多个分区之间的父子关系。 1 2 3 4 5 6 7 8 NAME = ini VERSION = v1 IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s [package] CLONE_URL = https://%(IMPORT_PATH)s [package.sub] package的没有父分区。\n如果某个键在子分区中不存在，则会在它的父分区中再次查找，直到没有父分区为止。 ini文件键值如果存在多行用\u0026quot;\u0026quot;\u0026quot;\u0026quot;\u0026quot;包裹。\n一行写不下可以使用\\，另起一行。\nIgnoreContinuation可以忽略连续行。 快速使用 go-ini 是第三方库，使用前需要安装[推荐】：\n1 $ go get -u gopkg.in/ini.v1 也可以使用 GitHub 上的仓库：\n1 $ go get -u github.com/go-ini/ini 为什么推荐gopkg.in,参考文章：gopkg.in介绍\n首先，创建一个my.ini配置文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 app_name = awesome web ; 这是注释 # possible values: DEBUG, INFO, WARNING, ERROR, FATAL log_level = DEBUG # 这也是注释 ; database=mysql [mysql] ip = 127.0.0.1 port = 3306 user = root password = 123456 database = awesome [redis] ip = 127.0.0.1 port = 6381 使用 go-ini 库读取：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) func main() { cfg, err := ini.Load(\u0026#34;my.ini\u0026#34;) if err != nil { log.Fatal(\u0026#34;Fail to read file: \u0026#34;, err) } fmt.Println(\u0026#34;App Name:\u0026#34;, cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;app_name\u0026#34;).String()) fmt.Println(\u0026#34;Log Level:\u0026#34;, cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;log_level\u0026#34;).String()) fmt.Println(\u0026#34;MySQL IP:\u0026#34;, cfg.Section(\u0026#34;mysql\u0026#34;).Key(\u0026#34;ip\u0026#34;).String()) mysqlPort, err := cfg.Section(\u0026#34;mysql\u0026#34;).Key(\u0026#34;port\u0026#34;).Int() if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;MySQL Port:\u0026#34;, mysqlPort) fmt.Println(\u0026#34;MySQL User:\u0026#34;, cfg.Section(\u0026#34;mysql\u0026#34;).Key(\u0026#34;user\u0026#34;).String()) fmt.Println(\u0026#34;MySQL Password:\u0026#34;, cfg.Section(\u0026#34;mysql\u0026#34;).Key(\u0026#34;password\u0026#34;).String()) fmt.Println(\u0026#34;MySQL Database:\u0026#34;, cfg.Section(\u0026#34;mysql\u0026#34;).Key(\u0026#34;database\u0026#34;).String()) fmt.Println(\u0026#34;Redis IP:\u0026#34;, cfg.Section(\u0026#34;redis\u0026#34;).Key(\u0026#34;ip\u0026#34;).String()) redisPort, err := cfg.Section(\u0026#34;redis\u0026#34;).Key(\u0026#34;port\u0026#34;).Int() if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Redis Port:\u0026#34;, redisPort) } 在 ini 文件中，每个键值对占用一行，中间使用=隔开，可以有空格，但不是必须得。以#开头的内容为注释。ini 文件是以分区（section）组织的。 分区以[name]开始，在下一个分区前结束。所有分区前的内容属于默认分区，如my.ini文件中的app_name和log_level。\n使用go-ini读取配置文件的步骤如下：\n首先调用ini.Load加载文件，得到配置对象cfg； 然后以分区名调用配置对象的Section方法得到对应的分区对象section，默认分区的名字为\u0026quot;\u0026quot;，也可以使用ini.DefaultSection； 以键名调用分区对象的Key方法得到对应的配置项key对象； 由于文件中读取出来的都是字符串，key对象需根据类型调用对应的方法返回具体类型的值使用，如上面的String、MustInt方法。 运行以下程序，得到输出：\n1 2 3 4 5 6 7 8 9 10 $ go run main.go App Name: awesome web Log Level: DEBUG MySQL IP: 127.0.0.1 MySQL Port: 3306 MySQL User: root MySQL Password: 123456 MySQL Database: awesome Redis IP: 127.0.0.1 Redis Port: 6381 配置文件中存储的都是字符串，所以类型为字符串的配置项不会出现类型转换失败的，故String()方法只返回一个值。 但如果类型为Int/Uint/Float64这些时，转换可能失败。所以Int()/Uint()/Float64()返回一个值和一个错误。\n要留意这种不一致！如果我们将配置中 redis 端口改成非法的数字 x6381，那么运行程序将报错：\n1 2 2024/05/16 11:45:00 strconv.ParseInt: parsing \u0026#34;x6381\u0026#34;: invalid syntax exit status 1 Must*便捷方法 如果每次取值都需要进行错误判断，那么代码写起来会非常繁琐。为此，go-ini也提供对应的MustType（Type 为Init/Uint/Float64等）方法，这个方法只返回一个值。 同时它接受可变参数，如果类型无法转换，取参数中第一个值返回，并且该参数设置为这个配置的值，下次调用返回这个值：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) func main() { cfg, err := ini.Load(\u0026#34;my.ini\u0026#34;) if err != nil { log.Fatal(\u0026#34;Fail to read file: \u0026#34;, err) } redisPort, err := cfg.Section(\u0026#34;redis\u0026#34;).Key(\u0026#34;port\u0026#34;).Int() if err != nil { fmt.Println(\u0026#34;before must, get redis port error:\u0026#34;, err) } else { fmt.Println(\u0026#34;before must, get redis port:\u0026#34;, redisPort) } fmt.Println(\u0026#34;redis Port:\u0026#34;, cfg.Section(\u0026#34;redis\u0026#34;).Key(\u0026#34;port\u0026#34;).MustInt(6381)) redisPort, err = cfg.Section(\u0026#34;redis\u0026#34;).Key(\u0026#34;port\u0026#34;).Int() if err != nil { fmt.Println(\u0026#34;after must, get redis port error:\u0026#34;, err) } else { fmt.Println(\u0026#34;after must, get redis port:\u0026#34;, redisPort) } } 配置文件还是 redis 端口为非数字 x6381 时的状态，运行程序：\n1 2 3 4 $ go run main.go before must, get redis port error: strconv.ParseInt: parsing \u0026#34;x6381\u0026#34;: invalid syntax redis Port: 6381 after must, get redis port: 6381 我们看到第一次调用Int返回错误，以 6381 为参数调用MustInt之后，再次调用Int，成功返回 6381。MustInt源码也比较简单：\n1 2 3 4 5 6 7 8 9 10 11 12 13 func (k *Key) MustInt(defaultVal ...int) int { val, err := k.Int() if len(defaultVal) \u0026gt; 0 \u0026amp;\u0026amp; err != nil { k.value = strconv.FormatInt(int64(defaultVal[0]), 10) return defaultVal[0] } return val } func (k *Key) Int() (int, error) { v, err := strconv.ParseInt(k.String(), 0, 64) return int(v), err } 加载ini文件对象 go-ini支持从多个数据源加载ini配置文件。\n数据源 可以是 []byte 类型的原始数据，string 类型的文件路径或 io.ReadCloser。可以加载这三个 任意多个 数据，如果是其他的类型会返回错误。调用Load(source interface{}, others ...interface{})函数。\n当创建好ini文件对象之后，我们还可以往里面添加数据源。调用func (f *File) Append(source interface{}, others ...interface{})方法。 1 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 package main import ( \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; \u0026#34;log\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;github.com/go-ini/ini\u0026#34; ) func main() { raw := []byte(`raw=原始数据`) noClose := strings.NewReader(\u0026#34;string=noClose\u0026#34;) cfg, err := ini.Load(raw, \u0026#34;./my.cfg\u0026#34;, io.NopCloser(noClose), io.NopCloser(bytes.NewBufferString(\u0026#34;close=have closer\u0026#34;)), ) if err != nil { log.Fatal(\u0026#34;Load error:\u0026#34;, err) } fmt.Println(cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;raw\u0026#34;).String()) fmt.Println(cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;username\u0026#34;).String()) fmt.Println(cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;string\u0026#34;).String()) fmt.Println(cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;close\u0026#34;).String()) err = cfg.Append(io.NopCloser(bytes.NewReader([]byte(\u0026#34;append=append\u0026#34;)))) if err != nil { log.Fatal(\u0026#34;Append error:\u0026#34;, err) } fmt.Println(cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;append\u0026#34;).String()) } io.NopCloser函数（no optiontion closer）是将没有Close方法的Reader添加Close方法（转换成实现ReadCloser接口的Reader），只不过是为了防止向bytes.NewReader、strings.NewReader这样的Reader没有Close方法，底层在自动关闭的时候出错。没有关闭操作的Reader，关闭时没任何操作，有的调用自身的Close方法。\n运行程序输出：\n1 2 3 4 5 6 $ go run main.go 原始数据 arlettebrook noClose have closer append 还可以创建一个没有任何数据源的文件对象。调用Empty函数。\n1 cfg := ini.Empty() 调用用LooseLoad的函数加载文件对象，若指定的文件不存在，不会返回错误。Load会返回错误。\n更牛逼的是，当那些之前不存在的文件在重新调用 Reload() 方法的时候突然出现了，那么它们会被正常加载。\n源码是：创建文件对象的时候会加载一次，创建完毕之后又会加载一次。 1 cfg, err := ini.LooseLoad(\u0026#34;filename\u0026#34;, \u0026#34;filename_404\u0026#34;) 默认情况下，当多个数据源中有相同的键时，后面的数据源会覆盖前面的数据源。\n调用 ShadowLoad函数，创建的数据源不会覆盖存在的值。 自定义加载ini文件对象 实现上调用Load、LooseLoad、InsensitiveLoad（后面会介绍）、ShadowLoad加载不同配置的文件对象，底层都是调用LoadSources(opts LoadOptions, source interface{}, others ...interface{})函数实现的。不同的配置是通过LoadOptions配置的。后面的参数都是数据源，默认必须有一个数据源：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func Load(source interface{}, others ...interface{}) (*File, error) { return LoadSources(LoadOptions{}, source, others...) } func LooseLoad(source interface{}, others ...interface{}) (*File, error) { return LoadSources(LoadOptions{Loose: true}, source, others...) } func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) { return LoadSources(LoadOptions{Insensitive: true}, source, others...) } func ShadowLoad(source interface{}, others ...interface{}) (*File, error) { return LoadSources(LoadOptions{AllowShadows: true}, source, others...) } 为了方便使用，都将不同的配置封装到了不同的函数。\n所以利用LoadSources我们可以实现自定义加载不同配置的文件对象。\n加载选项LoadOptions常用的属性：\nLoose：是否忽略文件路径不存在的错误。\nInsensitive：是否启用不敏感加载，作用：忽略键名的大小写。底层是将键都转换为小写。键名包括分区名。\nAllowShadows：是否不覆盖存在键的值。开启不覆盖之后，可以调用ValueWithShadows方法，获取指定分区下所有的重复键的值。\nUnescapeValueDoubleQuotes：是否强制忽略键值两端的双引号。用在多个双引号的值中。\nSkipUnrecognizableLines：是否跳过无法识别的行。默认无法识别就会报错。\nIgnoreContinuation：是否忽略连续换行。就是键值不支持换换行写\\。\nUnparseableSections：标记一个分区为无法解析。当获取无法解析的分区时，调用Body方法会获取该分区的原始数据，未标记无法获取，同时未标记一个无法解析的分区，解析会报错。除非开跳过无法解析的行。\nAllowBooleanKeys: 是否开启布尔键。开启允许只有一个键，而没有值。解析不会报错。值永远为true。保存时也只有键。\nAllowPythonMultilineValues：是否允许解析多行值，用于解析换行之后对齐的字符串。\n1 2 3 4 5 str = --- a b c --- 开启后类似上面的字符串都可以解析。\nIgnoreInlineComment：忽略行内注释。\nSpaceBeforeInlineComment：要求注释符号前必须带有一个空格\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 cfg, err := ini.LoadSources(ini.LoadOptions{ Loose: true, Insensitive: true, UnescapeValueDoubleQuotes: true, AllowShadows: true, IgnoreContinuation: true, SkipUnrecognizableLines: true, UnparseableSections: []string{\u0026#34;COMMENTS\u0026#34;}, }, \u0026#34;my.cfg\u0026#34;, \u0026#34;my.ini\u0026#34;) if err != nil { log.Fatal(err) } body := cfg.Section(\u0026#34;COMMENTS\u0026#34;).Body() fmt.Println(body) // \u0026lt;1\u0026gt;\u0026lt;L.Slide#2\u0026gt; This slide has the fuel listed in the wrong units \u0026lt;e.1\u0026gt; my.cfg:\n1 2 [COMMENTS] \u0026lt;1\u0026gt;\u0026lt;L.Slide#2\u0026gt; This slide has the fuel listed in the wrong units \u0026lt;e.1\u0026gt; 注意事项 默认情况下，本库会在您进行读写操作时采用锁机制来确保数据时间。但在某些情况下，您非常确定只进行读操作。此时，您可以通过设置 cfg.BlockMode = false 来将读操作提升大约 50-70% 的性能。\n操作分区（Section） 获取分区 在加载配置之后，可以通过Sections方法获取所有分区对象，是切片类型的*Section对象，SectionStrings()方法获取所有分区名。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) func main() { cfg, err := ini.Load(\u0026#34;my.ini\u0026#34;) if err != nil { log.Fatal(\u0026#34;Fail to read file: \u0026#34;, err) } sections := cfg.Sections() sectionStrings := cfg.SectionStrings() for k, v := range sections { fmt.Printf(\u0026#34;section%v: %s\\n\u0026#34;, k+1, v.Name()) } fmt.Print(\u0026#34;sections:\u0026#34;, sectionStrings) } 运行输出 3 个分区：\n1 2 3 4 5 $ go run main.go section1: DEFAULT section2: mysql section3: redis sections:[DEFAULT mysql redis] 调用GetSection(name)获取指定分区，如果分区不存在，会返回错误信息。返回的分区为nil。\n但调用Section(name)会获取指定分区，如果该分区不存在，则会自动创建指定空分区并返回：\n示例如下：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) func main() { cfg, err := ini.Load(\u0026#34;my.ini\u0026#34;) if err != nil { log.Fatal(\u0026#34;Fail to read file: \u0026#34;, err) } newSection, err := cfg.GetSection(\u0026#34;new\u0026#34;) if err != nil { fmt.Println(err) } fmt.Println(newSection) fmt.Println(cfg.SectionStrings()) newSection = cfg.Section(\u0026#34;new\u0026#34;) fmt.Println(newSection) fmt.Println(cfg.SectionStrings()) } 创建之后调用SectionStrings方法，新分区也会返回：\n1 2 3 4 5 6 $ go run main.go section \u0026#34;new\u0026#34; does not exist \u0026lt;nil\u0026gt; [DEFAULT mysql redis] \u0026amp;{0xc000152000 new map[] [] map[] false } [DEFAULT mysql redis new] 也可以手动创建一个新分区，如果分区已存在，则返回存在的分区：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) func main() { cfg, err := ini.Load(\u0026#34;my.ini\u0026#34;) if err != nil { log.Fatal(\u0026#34;Fail to read file: \u0026#34;, err) } fmt.Println(cfg.SectionStrings()) mysqlSection, err := cfg.NewSection(\u0026#34;mysql\u0026#34;) if err != nil { fmt.Println(err) } fmt.Println(mysqlSection.Keys()) fmt.Println(cfg.SectionStrings()) newSection, err := cfg.NewSection(\u0026#34;new\u0026#34;) if err != nil { fmt.Println(err) } fmt.Println(newSection.Keys()) fmt.Println(cfg.SectionStrings()) } 运行输出：\n1 2 3 4 5 6 $ go run main.go [DEFAULT mysql redis] [127.0.0.1 3306 root 123456 awesome] [DEFAULT mysql redis] [] [DEFAULT mysql redis new] 读取父子分区 递归读取键值 在获取所有键值的过程中，特殊语法 %(\u0026lt;name\u0026gt;)s 会被应用，其中 \u0026lt;name\u0026gt; 可以是相同分区或者默认分区下的键名。字符串 %(\u0026lt;name\u0026gt;)s 会被相应的键值所替代，如果指定的键不存在，则会用空字符串替代（我测试是保留字符串）。您可以最多使用 99 层的递归嵌套。\n在ini配置文件中，可以使用占位符%(name)s表示用之前已定义的键name的值来替换，这里的s表示值为字符串类型：\n1 2 3 4 5 6 7 8 9 10 # parent_child.ini NAME = ini VERSION = v1 IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s USERNAME = arlettebrook [package] CLONE_URL = https://%(IMPORT_PATH)s [package.sub] 上面在默认分区中设置IMPORT_PATH的值时，使用了前面定义的NAME和VERSION。 在package分区中设置CLONE_URL的值时，使用了默认分区中定义的IMPORT_PATH。\n我们还可以在分区名中使用.表示两个或多个分区之间的父子关系，例如package.sub的父分区为package，package没有父分区。 如果某个键在子分区中不存在，则会在它的父分区中再次查找，直到没有父分区为止：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) func main() { cfg, err := ini.Load(\u0026#34;parent_child.ini\u0026#34;) if err != nil { log.Fatal(\u0026#34;Fail to read file: \u0026#34;, err) } fmt.Println(\u0026#34;Clone url from package.sub:\u0026#34;, cfg.Section(\u0026#34;package.sub\u0026#34;).Key(\u0026#34;CLONE_URL\u0026#34;).String()) fmt.Println(\u0026#34;package没有父分区:\u0026#34;, cfg.Section(\u0026#34;package\u0026#34;).Key(\u0026#34;USERNAME\u0026#34;).String() == \u0026#34;\u0026#34;) } 运行程序输出：\n1 2 3 $ go run main.go Clone url from package.sub: https://gopkg.in/ini.v1 package没有父分区: true 子分区中package.sub中没有键CLONE_URL，返回了父分区package中的值。\npackage分区中没有USERNAME,它并没有父分区，所以返回空字符串。（调用Key方法如果键不存在，会创建该键，值为空字符串。）后面会介绍。\n​\n操作键（Key） 获取键 在指定分区调用GetKey方法，可以获取指定的键。如果键不存在，会返回Error对象和nil。 和分区一样，也可以直接获取键而忽略错误处理，调用Key方法获取指定的键，如果键不存在，会创建该键，值为空字符串。 示例如下：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) func main() { cfg, err := ini.Load(\u0026#34;my.ini\u0026#34;) if err != nil { log.Fatal(\u0026#34;Fail to read file: \u0026#34;, err) } key, err := cfg.Section(ini.DefaultSection).GetKey(\u0026#34;app_name123\u0026#34;) if err != nil { fmt.Println(err) } fmt.Println(key) nothingness := cfg.Section(\u0026#34;mysql\u0026#34;).Key(\u0026#34;app_name123\u0026#34;) fmt.Println(nothingness.String() == \u0026#34;\u0026#34;) } 运行程序输出：\n1 2 3 4 $ go run main.go error when getting key of section \u0026#34;DEFAULT\u0026#34;: key \u0026#34;app_name123\u0026#34; not exists \u0026lt;nil\u0026gt; true 默认分区中，不存在app_name123所以GetKy返回Error和nil。而Key方法返回值为空字符串的*Key类型。\n键的其他操作 在某个分区下，调用HasKey方法，能判断该键是否存在。\n在某个分区下，调用NewKey方法，能够在指定分区下创建键，有两个参数，第一个：键名，第二个：值。\n这与创建分区不一样，分区如果存在，会返回存在的分区。 键如果存在，会覆盖值。 在某个分区下，调用Keys方法，能够获取指定分区下所有的*Key对象，是[]*Key类型。\n与SectionStrings方法差不多，调用KeyStrings方法，能够获取所有的键名，是[]string类型。 在某分区下，调用KeysHash方法，能够获取该分区下的所有键值对的map集合。键和值的类型都为string。\n示例如下:\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) func main() { cfg, err := ini.Load(\u0026#34;my.ini\u0026#34;) if err != nil { log.Fatal(\u0026#34;Fail to read file: \u0026#34;, err) } key := cfg.Section(ini.DefaultSection).HasKey(\u0026#34;app_name\u0026#34;) if key { fmt.Println(cfg.Section(ini.DefaultSection).Key(\u0026#34;app_name\u0026#34;).String()) } newKey, err := cfg.Section(ini.DefaultSection).NewKey(\u0026#34;app_name\u0026#34;, \u0026#34;awesome go\u0026#34;) if err != nil { log.Fatal(err) } fmt.Println(newKey.String()) fmt.Printf(\u0026#34;%#v\\n\u0026#34;, cfg.Section(\u0026#34;\u0026#34;).Keys()) fmt.Println(cfg.Section(\u0026#34;\u0026#34;).KeyStrings()) keysHash := cfg.Section(\u0026#34;\u0026#34;).KeysHash() for k, v := range keysHash { fmt.Printf(\u0026#34;%s=%s\\n\u0026#34;, k, v) } } 运行程序输出：\n1 2 3 4 5 6 7 $ go run main.go awesome web awesome go []*ini.Key{(*ini.Key)(0xc00010a690), (*ini.Key)(0xc00010a700)} [app_name log_level] app_name=awesome go log_level=DEBUG 获取上级父分区下所有的键对象。\n1 cfg.Section(\u0026#34;package.sub\u0026#34;).ParentKeys() 当键名为-表示自增键名，在程序中是从#1开始，#number表示，分区之间是相互独立的。\n1 2 3 4 [features] -: Support read/write comments of keys and sections -: Support auto-increment of key names -: Support load multiple files to overwrite key values 1 cfg.Section(\u0026#34;features\u0026#34;).KeyStrings() // []{\u0026#34;#1\u0026#34;, \u0026#34;#2\u0026#34;, \u0026#34;#3\u0026#34;} 忽略键名的大小写 默认情况下分区名和键名都区分大小写，当调用ini.InsensitiveLoad方法加载配置文件时，能够将所有分区和键名在读取里强制转换为小写，这样当在获取分区或者键的时候，所指定的分区名或键名不区分大小写：\n1 2 3 4 5 6 7 8 9 10 cfg, err := ini.InsensitiveLoad(\u0026#34;filename\u0026#34;) //... // sec1 和 sec2 指向同一个分区对象 sec1, err := cfg.GetSection(\u0026#34;Section\u0026#34;) sec2, err := cfg.GetSection(\u0026#34;SecTIOn\u0026#34;) // key1 和 key2 指向同一个键对象 key1, err := sec1.GetKey(\u0026#34;Key\u0026#34;) key2, err := sec2.GetKey(\u0026#34;KeY\u0026#34;) 为什么在加载的时候开启转换为小写，在调用的时候就能忽略大小？ 因为在调用的时候会判断是否开启转换为小写，是会将查询的分区名或键名强制转换为小写。都转换为小写了，也就能够获取了。 ​\n操作键值（Value） 获取一个类型为字符串（string）的值：\n1 val := cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;key name\u0026#34;).String() 获取值的同时通过自定义函数进行处理验证：\n1 2 3 4 5 6 val := cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;key name\u0026#34;).Validate(func(in string) string { if len(in) == 0 { return \u0026#34;default\u0026#34; } return in }) 如果您不需要任何对值的自动转变功能（例如递归读取），可以直接获取原值（这种方式性能最佳）：\n1 val := cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;key name\u0026#34;).Value() 判断某个原值是否存在：\n1 yes := cfg.Section(\u0026#34;\u0026#34;).HasValue(\u0026#34;test value\u0026#34;) 获取其它类型的值调用对应类型的方法。返回值带有Error信息，如果不需要Error信息可以调用MustXxx方法。该方法可以指定默认值，用于转换失败的默认值。没有指定默认值为对应类型的零值。\n1 2 3 4 5 6 7 8 v, err = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;INT\u0026#34;).Int() v, err = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;TIME\u0026#34;).TimeFormat(time.RFC3339) v, err = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;TIME\u0026#34;).Time() // RFC3339 v = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;INT\u0026#34;).MustInt() v = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;INT\u0026#34;).MustInt(10) v = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;TIME\u0026#34;).MustTimeFormat(time.RFC3339, time.Now()) v = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;TIME\u0026#34;).MustTime(time.Now()) // RFC3339 \u0026quot;\u0026quot;\u0026quot;\u0026quot;\u0026quot;\u0026quot;包裹的多行字符串跟普通的获取方式一样。\n\\：一行写不下换行写，也是跟普通的获取方式一样，只不过属性IgnoreContinuation，可以忽略连续换行。就是\\不起作用。\n默认情况下字符串中只有两端有引号，无论是单、双、三，都会自动剔除。但当字符串里面有与两端相同的引号，那么引号都会保留。\nUnescapeValueDoubleQuotes属性会移除两端的双引号，只能是双引号。 获取值的时候我们还可以指定候选。如果配置文件中的值不是候选中的值，那么将选用默认值，默认值可以不是候选里面的值。string类型是In方法，其他的是InXxx方法\n1 2 v := cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;STRING\u0026#34;).In(\u0026#34;default\u0026#34;, []string{\u0026#34;str\u0026#34;, \u0026#34;arr\u0026#34;, \u0026#34;types\u0026#34;}) v = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;INT\u0026#34;).InInt(10, []int{10, 20, 30}) 验证获取的值是否在指定范围内：有三个参数：第一个：没有在范围内的默认值。第二个：最小值。第三个：最大值。string类型没有范围。\n1 2 3 4 vals = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;FLOAT64\u0026#34;).RangeFloat64(0.0, 1.1, 2.2) vals = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;INT\u0026#34;).RangeInt(0, 10, 20) vals = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;TIME\u0026#34;).RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) vals = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;TIME\u0026#34;).RangeTime(time.Now(), minTime, maxTime) // RFC3339 自动分割键值到切片（slice）。作用：获取一个键的多个值。方法是对应类型加s，并指定分隔符。\n当存在无效输入时，使用零值代替。 注意分隔符不能为空字符串，会出现死循环。可以为空格。 当在前面加上ValidXxxs，存在无效输入时，会忽略掉。 当在前面加上StrictXxxs，存在无效输入时，直接返回错误。 1 2 3 vals = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;INTS\u0026#34;).Ints(\u0026#34;,\u0026#34;) vals = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;INTS\u0026#34;).ValidInts(\u0026#34;,\u0026#34;) vals = cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;INTS\u0026#34;).StrictInts(\u0026#34;,\u0026#34;) 修改键的值，调用SetValue方法。\n1 2 username := cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;username\u0026#34;) username.SetValue(\u0026#34;Mark\u0026#34;) 在某分区下调用NewBooleanKey方法，会创建布尔键，值永远为true。保存时只有键名。解析时注意要开启AllowBooleanKeys，否则会报错。\n1 key, err := sec.NewBooleanKey(\u0026#34;skip-host-cache\u0026#34;) 默认情况下后面出现的键会覆盖前面存在的键，当开启AllowShadows配置选项时，就是调用ShadowLoad加载数据源。后出现的键不会覆盖前面的值。还可以通过ValueWithShadows方法获取指定分区下重复键的所有值。\n​\n操作注释（Comment） 下述几种情况的内容将被视为注释：\n所有以 # 或 ; 开头的行 所有在 # 或 ; 之后的内容 分区标签后的文字 (即 [分区名] 之后的内容) 如果你希望使用包含 # 或 ; 的值，请使用 ``` 或 \u0026quot;\u0026quot;\u0026quot; 进行包覆。\n除此之外，您还可以通过 LoadOptions 完全忽略行内注释：\n1 2 3 cfg, err := ini.LoadSources(ini.LoadOptions{ IgnoreInlineComment: true, }, \u0026#34;app.ini\u0026#34;) 或要求注释符号前必须带有一个空格：\n1 2 3 cfg, err := ini.LoadSources(ini.LoadOptions{ SpaceBeforeInlineComment: true, }, \u0026#34;app.ini\u0026#34;) 在分区或者键上调用Comment属性，会获取该分区或者键的所有注释（能获取头上和后边的）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) func main() { cfg, _ := ini.Load(\u0026#34;my.ini\u0026#34;) c1 := cfg.Section(\u0026#34;mysql\u0026#34;).Comment fmt.Println(c1) fmt.Println() c2 := cfg.Section(\u0026#34;\u0026#34;).Key(\u0026#34;log_level\u0026#34;).Comment fmt.Println(c2) } 运行程序输出：\n1 2 3 4 5 $ go run main.go ; database=mysql # possible values: DEBUG, INFO, WARNING, ERROR, FATAL # 这也是注释 保存配置 将配置保存到某个文件，调用SaveTo或SaveToIndent，第二个方法多一个参数，用于指定分区下键的缩进（除默认分区），可以是\\t等：\n1 2 3 // ... err = cfg.SaveTo(\u0026#34;my.ini\u0026#34;) err = cfg.SaveToIndent(\u0026#34;my.ini\u0026#34;, \u0026#34;\\t\u0026#34;) 还可以写入到任何实现 io.Writer 接口的对象中，也是提供了两个方法WriteTo、WriteToIndent：第二个可以指定分区下键的缩进（除默认分区）：\n1 2 3 // ... cfg.WriteTo(writer) cfg.WriteToIndent(writer, \u0026#34;\\t\u0026#34;) 默认情况下，空格将被用于对齐键值之间的等号以美化输出结果，以下代码可以禁用该功能：\n1 ini.PrettyFormat = false 下面我们通过程序生成前面使用的配置文件my.ini并保存：\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 45 46 47 48 49 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) func main() { cfg := ini.Empty() defaultSection := cfg.Section(\u0026#34;\u0026#34;) defaultSection.NewKey(\u0026#34;app_name\u0026#34;, \u0026#34;awesome web\u0026#34;) defaultSection.NewKey(\u0026#34;log_level\u0026#34;, \u0026#34;DEBUG\u0026#34;) mysqlSection, err := cfg.NewSection(\u0026#34;mysql\u0026#34;) if err != nil { fmt.Println(\u0026#34;new mysql section failed:\u0026#34;, err) return } mysqlSection.NewKey(\u0026#34;ip\u0026#34;, \u0026#34;127.0.0.1\u0026#34;) mysqlSection.NewKey(\u0026#34;port\u0026#34;, \u0026#34;3306\u0026#34;) mysqlSection.NewKey(\u0026#34;user\u0026#34;, \u0026#34;root\u0026#34;) mysqlSection.NewKey(\u0026#34;password\u0026#34;, \u0026#34;123456\u0026#34;) mysqlSection.NewKey(\u0026#34;database\u0026#34;, \u0026#34;awesome\u0026#34;) redisSection, err := cfg.NewSection(\u0026#34;redis\u0026#34;) if err != nil { fmt.Println(\u0026#34;new redis section failed:\u0026#34;, err) return } redisSection.NewKey(\u0026#34;ip\u0026#34;, \u0026#34;127.0.0.1\u0026#34;) redisSection.NewKey(\u0026#34;port\u0026#34;, \u0026#34;6381\u0026#34;) err = cfg.SaveTo(\u0026#34;my.ini\u0026#34;) if err != nil { fmt.Println(\u0026#34;SaveTo failed: \u0026#34;, err) } err = cfg.SaveToIndent(\u0026#34;my-pretty.ini\u0026#34;, \u0026#34;\\t\u0026#34;) if err != nil { fmt.Println(\u0026#34;SaveToIndent failed: \u0026#34;, err) } cfg.WriteTo(os.Stdout) fmt.Println() cfg.WriteToIndent(os.Stdout, \u0026#34;\\t\u0026#34;) } 运行程序，生成两个文件my.ini和my-pretty.ini，同时控制台输出文件内容。\nmy.ini：\n1 2 3 4 5 6 7 8 9 10 11 12 13 app_name = awesome web log_level = DEBUG [mysql] ip = 127.0.0.1 port = 3306 user = root password = 123456 database = awesome [redis] ip = 127.0.0.1 port = 6381 my-pretty.ini：\n1 2 3 4 5 6 7 8 9 10 11 12 13 app_name = awesome web log_level = DEBUG [mysql] ip = 127.0.0.1 port = 3306 user = root password = 123456 database = awesome [redis] ip = 127.0.0.1 port = 6381 *Indent方法会对默认分区以外分区下的键增加缩进，看起来美观一点。\n分区与结构体字段映射 映射到结构体 调用MapTo函数或者方法，可以将文件对象映射到结构体。\n当MapTo为方法时，对象是文件对象或分区，参数是要映射的结构体。 为了使用方便，直接将MapTo封装成了函数，该函数接收两个参数，第一个参数：结构体。第二个：数据源。 当对象为分区时，映射到一个分区 创建结构体的时候可以指定默认值，如果数据源没有或类型解析错误将使用默认值 示例如下：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) type Config struct { AppName string `ini:\u0026#34;app_name\u0026#34;` LogLevel string `ini:\u0026#34;log_level\u0026#34;` MySQL MySQLConfig `ini:\u0026#34;mysql\u0026#34;` Redis RedisConfig `ini:\u0026#34;redis\u0026#34;` } type MySQLConfig struct { IP string `ini:\u0026#34;ip\u0026#34;` Port int `ini:\u0026#34;port\u0026#34;` User string `ini:\u0026#34;user\u0026#34;` Password string `ini:\u0026#34;password\u0026#34;` Database string `ini:\u0026#34;database\u0026#34;` } type RedisConfig struct { IP string `ini:\u0026#34;ip\u0026#34;` Port int `ini:\u0026#34;port\u0026#34;` } func main() { cfg, err := ini.Load(\u0026#34;my.ini\u0026#34;) if err != nil { log.Fatal(\u0026#34;load my.ini failed: \u0026#34;, err) } assertMapToError := func(e error) { if e != nil { log.Fatal(\u0026#34;MapTo error:\u0026#34;, err) } } c1 := new(Config) err = cfg.MapTo(\u0026amp;c1) assertMapToError(err) fmt.Println(c1) c2 := new(Config) err = ini.MapTo(c2, \u0026#34;my.ini\u0026#34;) assertMapToError(err) fmt.Println(c2) m := \u0026amp;MySQLConfig{ IP: \u0026#34;localhost\u0026#34;, } err = cfg.Section(\u0026#34;mysql\u0026#34;).MapTo(m) assertMapToError(err) fmt.Println(m) } MapTo内部使用了反射，所以结构体字段必须都是导出的。如果键名与字段名不相同，那么需要在结构标签中指定对应的键名。 这一点与 Go 标准库encoding/json和encoding/xml不同。标准库json/xml解析时可以将键名app_name对应到字段名AppName。而go-ini需要[自定义键名映射器](#键名映射器（Name Mapper）)才能实现这种效果。\n运行程序输出：\n1 2 3 4 $ go run main.go \u0026amp;{awesome web DEBUG {127.0.0.1 3306 root 123456 awesome} {127.0.0.1 6381}} \u0026amp;{awesome web DEBUG {127.0.0.1 3306 root 123456 awesome} {127.0.0.1 6381}} \u0026amp;{127.0.0.1 3306 root 123456 awesome} 从结构体反射 我们可以调用ReflectFrom函数或方法，将结构体反射成文件对象。\n当为方法时，对象是反射到的文件对象或分区，参数是结构体。\n为了使用方便，将其封装成了函数，接收两个参数。第一个：反射到的文件对象，第二个：结构体 当对象为分区时，反射到分区。\n注意当结构体字段与配置键不同名时需要用结构体标签指定。\n支持的标签：\nini：指定键名，或者分区名。\n有第二个参数omitempty，用，分隔开。值为空时，省略掉，不写入文件对象。\n有第三参数allowshadow，如果不需要前两个标签规则，可以使用 ini:\u0026quot;,,allowshadow\u0026quot; 进行简写。\n作用：将一个键的不同值分行保存，不用分隔符分开。 1 2 3 4 [IP] value = 192.168.31.201 value = 192.168.31.211 value = 192.168.31.221 1 2 3 type IP struct { Value []string `ini:\u0026#34;value,omitempty,allowshadow\u0026#34;` } comment：指定注释，保存到配置注释会在键的头上。\ndelim：指定分隔符。一个键存在多个值的情况，需要指定分隔符。\n1 2 3 4 5 6 7 8 9 10 11 12 13 type Embeded struct { Dates []time.Time `delim:\u0026#34;|\u0026#34; comment:\u0026#34;Time data\u0026#34;` Places []string `ini:\u0026#34;places,omitempty\u0026#34;` None []int `ini:\u0026#34;,omitempty\u0026#34;` } ... Embeded{ []time.Time{time.Now(), time.Now()}, []string{\u0026#34;HangZhou\u0026#34;, \u0026#34;Boston\u0026#34;}, []int{}, } ... 1 2 3 4 5 ; Embeded section [Embeded] ; Time data Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00 places = HangZhou,Boston 示例如下：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) type Config struct { AppName string `ini:\u0026#34;app_name\u0026#34;` LogLevel string `ini:\u0026#34;log_level\u0026#34;` MySQL MySQLConfig `ini:\u0026#34;mysql\u0026#34;` Redis RedisConfig `ini:\u0026#34;redis\u0026#34;` } type MySQLConfig struct { IP string `ini:\u0026#34;ip\u0026#34;` Port int `ini:\u0026#34;port\u0026#34;` User string `ini:\u0026#34;user\u0026#34;` Password string `ini:\u0026#34;password\u0026#34;` Database string `ini:\u0026#34;database\u0026#34;` } type RedisConfig struct { IP string `ini:\u0026#34;ip\u0026#34;` Port int `ini:\u0026#34;port\u0026#34;` } func main() { cfg1 := ini.Empty() c1 := Config{ AppName: \u0026#34;awesome web\u0026#34;, LogLevel: \u0026#34;DEBUG\u0026#34;, MySQL: MySQLConfig{ IP: \u0026#34;127.0.0.1\u0026#34;, Port: 3306, User: \u0026#34;root\u0026#34;, Password: \u0026#34;123456\u0026#34;, Database: \u0026#34;awesome\u0026#34;, }, Redis: RedisConfig{ IP: \u0026#34;127.0.0.1\u0026#34;, Port: 6381, }, } assertReflectError := func(e error) { if e != nil { log.Fatal(\u0026#34;Reflect error:\u0026#34;, e) } } err := ini.ReflectFrom(cfg1, \u0026amp;c1) assertReflectError(err) fmt.Println(cfg1.Section(\u0026#34;\u0026#34;).Key(\u0026#34;app_name\u0026#34;).String()) c2 := Config{ AppName: \u0026#34;awesome go\u0026#34;, } cfg2 := ini.Empty() err = cfg2.ReflectFrom(\u0026amp;c2) assertReflectError(err) fmt.Println(cfg2.Section(\u0026#34;\u0026#34;).Key(\u0026#34;app_name\u0026#34;).String()) m := MySQLConfig{ IP: \u0026#34;localhost\u0026#34;, } cfg3 := ini.Empty() err = cfg3.Section(\u0026#34;mysql\u0026#34;).ReflectFrom(\u0026amp;m) assertReflectError(err) fmt.Println(cfg3.Section(\u0026#34;mysql\u0026#34;).Key(\u0026#34;ip\u0026#34;).String()) } 运行程序输出：\n1 2 3 4 $ go run main.go awesome web awesome go localhost 映射/反射的其它说明 任何嵌入的结构都会被默认认作一个不同的分区，并且不会自动产生所谓的父子分区关联：\n1 2 3 4 5 6 7 8 9 10 11 12 13 type Child struct { Age string } type Parent struct { Name string Child } type Config struct { City string Parent } 示例配置文件：\n1 2 3 4 5 6 7 City = Boston [Parent] Name = Unknwon [Child] Age = 21 如果需要指定嵌入结构体是同一个分区，需要指定标签指定分区名如：ini:\u0026ldquo;Parent\u0026rdquo;。示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 type Child struct { Age string } type Parent struct { Name string Child `ini:\u0026#34;Parent\u0026#34;` } type Config struct { City string Parent } 示例配置文件：\n1 2 3 4 5 City = Boston [Parent] Name = Unknwon Age = 21 自定义键名和键值映射器 键名映射器（Name Mapper） 当我们利用结构体标签指定键名时，会觉得太麻烦。为了节省时间并简化代码，go-ini库支持类型为 NameMapper 的名称映射器，该映射器负责结构字段名与分区名和键名之间的映射。\n目前有 2 款内置的映射器：\nAllCapsUnderscore：该映射器将字段名转换至格式 ALL_CAPS_UNDERSCORE 后再去匹配分区名和键名。 TitleUnderscore：该映射器将字段名转换至格式 title_underscore 后再去匹配分区名和键名。 使用方法：只需要将映射MapTo、反射ReflectFrom函数后面加上WithMapper，传惨时，传入对应映射器即可。或者给指定的文件对象指定映射器。属性是 NameMapper，示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type Info struct { PackageName string } func main() { err = ini.MapToWithMapper(\u0026amp;Info{}, ini.TitleUnderscore, []byte(\u0026#34;package_name=ini\u0026#34;)) // ... cfg, err := ini.Load([]byte(\u0026#34;PACKAGE_NAME=ini\u0026#34;)) // ... info := new(Info) cfg.NameMapper = ini.AllCapsUnderscore err = cfg.MapTo(info) // ... err = ini.ReflectFromWithMapper(cfg, \u0026amp;Info{}, ini.TitleUnderscore) } 键值映射器（Value Mapper） 值映射器允许使用一个自定义函数自动展开值的具体内容，例如在运行时获取环境变量：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; ) type Env struct { Foo string `ini:\u0026#34;foo\u0026#34;` } func main() { cfg, _ := ini.Load([]byte(\u0026#34;[env]\\nfoo = ${USERNAME}\\n\u0026#34;)) cfg.ValueMapper = os.ExpandEnv env := \u0026amp;Env{} _ = cfg.Section(\u0026#34;env\u0026#34;).MapTo(env) fmt.Println(env) } 运行程序输出：\n1 2 $ go run main.go \u0026amp;{Lenovo} 会输出你电脑的用户名。\n总结 本文简单介绍了ini配置文件格式，内容来自互联网，仅供参考。还介绍了go-ini库，基本上参考的是其官方文档，官方文档写的非常详细，推荐去看，而且有中文。 作者是无闻，相信做 Go 开发的都不陌生。\n参考 ini配置文件格式 go-ini GitHub 仓库 go-ini 官方文档 Go 每日一库之 go-ini ","date":"2024-05-15T22:26:10+08:00","permalink":"https://arlettebrook.github.io/p/go-ini-introduction/","title":"Go-ini Introduction"},{"content":" 简介 一线开发人员每天都要使用日期和时间相关的功能，各种定时器，活动时间处理等。标准库time使用起来不太灵活，特别是日期时间的创建和运算。carbon库是一个时间扩展库，基于 PHP 的carbon库编写。提供易于使用的接口。Go社区还有另外一个同名库carbon，我称之为增强版，本文就来介绍一下这两个库，主要介绍低配版，二者差不多，会了低配版，增强版也就会了。增强版用的人更多，推荐使用增强版。\n快速使用 第三方库需要先安装：\n1 $ go get -u github.com/uniplaces/carbon 后使用：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/uniplaces/carbon\u0026#34; ) func main() { fmt.Printf(\u0026#34;Right now is %s\\n\u0026#34;, carbon.Now().DateTimeString()) today, _ := carbon.NowInLocation(\u0026#34;Japan\u0026#34;) fmt.Printf(\u0026#34;Right now in Japan is %s\\n\u0026#34;, today) fmt.Printf(\u0026#34;Tomorrow is %s\\n\u0026#34;, carbon.Now().AddDay()) fmt.Printf(\u0026#34;Last week is %s\\n\u0026#34;, carbon.Now().SubWeek()) nextOlympics, _ := carbon.CreateFromDate(2016, time.August, 5, \u0026#34;Europe/London\u0026#34;) nextOlympics = nextOlympics.AddYears(4) fmt.Printf(\u0026#34;Next olympics are in %d\\n\u0026#34;, nextOlympics.Year()) if carbon.Now().IsWeekend() { fmt.Printf(\u0026#34;Happy time!\u0026#34;) } } carbon库的使用很便捷，首先它完全兼容标准库的time.Time类型，实际上该库的日期时间类型Carbon直接将time.Time内嵌到结构中（继承了time.Time结构体），所以time.Time的方法可直接调用：\n1 2 3 4 5 6 7 8 9 // github.com/uniplaces/carbon/carbon.go type Carbon struct { time.Time weekStartsAt time.Weekday weekEndsAt time.Weekday weekendDays []time.Weekday stringFormat string Translator *Translator } 其次，简化了创建操作。标准库time创建一个Time对象，如果不是本地或 UTC 时区，需要自己先调用LoadLocation加载对应时区。然后将该时区对象传给time.Time.In或者time.Date方法创建。carbon可以直接传时区名字。底层其实也是用原始方法创建的，carbon帮我们封装到了NowInLocation方法中。只需要传入时区，就能获取指定时区当前时间。\ncarbon还提供了很多方法做日期运算，如例子中的AddDay，SubWeek，Addyears等。没有s默认+1，有s指定加多少，注意单位，都是见名知义的。\n（上面的源码很简单，看不懂的话，建议去看一下源码）\nCreateFromDate方法，用于指定日期、时区创建Carbon对象。时间部分默认是当前时间。如果没有指定时区(为空字符串），默认为UTC时区。字符串为Local,为本地时区。\n一点点源码：\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 // github.com\\uniplaces\\carbon\\carbon.go func CreateFromDate(y int, mon time.Month, d int, location string) (*Carbon, error) { h, m, s := Now().Clock() ns := Now().Nanosecond() return Create(y, mon, d, h, m, s, ns, location) func Create(y int, mon time.Month, d, h, m, s, ns int, location string) (*Carbon, error) { l, err := time.LoadLocation(location) if err != nil { return nil, err } return create(y, mon, d, h, m, s, ns, l), nil } // time\\zoneinfo.go func LoadLocation(name string) (*Location, error) { if name == \u0026#34;\u0026#34; || name == \u0026#34;UTC\u0026#34; { return UTC, nil } if name == \u0026#34;Local\u0026#34; { return Local, nil } ... // time\\time.go func Now() Time { sec, nsec, mono := now() mono -= startNano sec += unixToInternal - minWall if uint64(sec)\u0026gt;\u0026gt;33 != 0 { // Seconds field overflowed the 33 bits available when // storing a monotonic time. This will be true after // March 16, 2157. return Time{uint64(nsec), sec + minWall, Local} } return Time{hasMonotonic | uint64(sec)\u0026lt;\u0026lt;nsecShift | uint64(nsec), mono, Local} // 这里用本地时区。 } 总结：没有指定时区时，默认本地区时区，当要指定时区时，默认UTC时区。（底层源码目前看不懂😁😁😁）\n时区 在介绍其它内容之前，我们先说一说这个时区的问题。以下引用维基百科的描述：\n时区是地球上的区域使用同一个时间定义。以前，人们通过观察太阳的位置（时角）决定时间，这就使得不同经度的地方的时间有所不同（地方时）。1863年，首次使用时区的概念。时区通过设立一个区域的标准时间部分地解决了这个问题。 世界各国位于地球不同位置上，因此不同国家，特别是东西跨度大的国家日出、日落时间必定有所偏差。这些偏差就是所谓的时差。\n例如，日本东京位于东九区，北京位于东八区，所以日本比中国快一个小时，日本14:00的时候中国13:00。\n在 Linux 中，时区一般存放在类似/usr/share/zoneinfo这样的目录。这个目录中有很多文件，每个时区一个文件。时区文件是二进制文件，可以执行info tzfile查看具体格式。\n时区名称的一般格式为city，或country/city，或continent/city。即要么就是一个城市名，要么是国家名+/+城市名，要么是洲名+/+城市名。\n例如上海时区为Asia/Shanghai(在时区文件中表示东八区的时区，即北京时间），香港时区为Asia/Hong_Kong或者Hongkong（常识：中国标准时间（CST)为东八区时间,范围中国全境（大陆、港澳、台湾））意味着香港时间与北京时间一样，注意北京时间并不是北京的地方时，东八区在东经120°的地方，北京在东经116.4°。故东经120度地方时比北京的地方时早约14分半钟(了解）。还有CST,可能为其他地区的标准时间，如美国中部标准时间（Central Standard Time），使用的时候要注意上下文。\n也有一些特殊的，如 UTC，Local等。\nGo 语言为了可移植性，在安装包中提供了时区文件，在Go安装目录下的lib/time/zoneinfo.zip文件，大家可以执行解压看看😀。\n使用 Go 标准库time创建某个时区的时间，需要先加载时区：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; ) func main() { loc, err := time.LoadLocation(\u0026#34;Japan\u0026#34;) if err != nil { log.Fatal(\u0026#34;failed to load location: \u0026#34;, err) } //t := time.Now().In(loc) // 这里演示指定日期时间创建Time对象 d := time.Date(2020, time.July, 24, 20, 0, 0, 0, loc) fmt.Printf(\u0026#34;The opening ceremony of next olympics will start at %s in Japan\\n\u0026#34;, d) } 使用carbon就不用这么麻烦：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/uniplaces/carbon\u0026#34; ) func main() { c, err := carbon.Create(2020, time.July, 24, 20, 0, 0, 0, \u0026#34;Japan\u0026#34;) if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;The opening ceremony of next olympics will start at %s in Japan\\n\u0026#34;, c) } 创建对象 创建Carbon对象，可以通过：\nNow方法，返回本地时区的当前时间对象。\nNewCarbon方法，返回指定时间的本地时区时间对象。Now方法是基于它创建的。\nCreateFromDate方法，用于指定日期、时区创建Carbon对象。时间部分默认是当前时间。如果没有指定时区(为空字符串），默认为UTC时区。字符串为Local,为本地时区。\nCrreateFromTime方法一样，只不过是指定时间。 注意，创建的日期，默认为当前日期。虽然没有指定，但不会没有。 Create方法，返回根据日期、时间、时区创建的时间对象\nParse方法，返回根据日期时间格式、指定的时间、时区解析的时间对象。\n示例如下：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/uniplaces/carbon\u0026#34; ) func main() { now := carbon.NewCarbon(time.Now()) fmt.Printf(\u0026#34;New carbon from time instance: %s\\n\u0026#34;, now) now = carbon.Now() fmt.Printf(\u0026#34;New carbon from Now function: %s\\n\u0026#34;, now) fromDate, _ := carbon.CreateFromDate(2000, 1, 1, \u0026#34;Europe/London\u0026#34;) fmt.Printf(\u0026#34;Created from date: %s\\n\u0026#34;, fromDate) fromTime, _ := carbon.CreateFromTime(9, 16, 11, 0, \u0026#34;Europe/Madrid\u0026#34;) fmt.Printf(\u0026#34;Created from time: %s\\n\u0026#34;, fromTime) create, _ := carbon.Create(2024, 5, 14, 15, 3, 3, 3, \u0026#34;Local\u0026#34;) fmt.Println(create) parsed, _ := carbon.Parse(carbon.DateFormat, \u0026#34;2000-08-20\u0026#34;, \u0026#34;Europe/Berlin\u0026#34;) fmt.Printf(\u0026#34;Parsed time: %s\\n\u0026#34;, parsed) parse, _ := carbon.Parse(time.DateTime, \u0026#34;2024-05-14 22:10:22\u0026#34;, \u0026#34;Local\u0026#34;) fmt.Println(parse) timestamp, _ := carbon.CreateFromTimestamp(-1, \u0026#34;Local\u0026#34;) fmt.Println(timestamp) // 1970-01-01 07:59:59 } 后面会介绍时间格式化。\n时间运算 使用标准库time的时间运算需要先定义一个time.Duration对象，time库预定义的只有纳秒到小时的精度：\n1 2 3 4 5 6 7 8 const ( Nanosecond Duration = 1 Microsecond = 1000 * Nanosecond Millisecond = 1000 * Microsecond Second = 1000 * Millisecond Minute = 60 * Second Hour = 60 * Minute ) 其它的时长就需要自己使用time.ParseDuration构造了，而且time.ParseDuration不能构造其它精度的时间。 如果想要增加/减少年月日，就需要使用time.Time的AddDate方法：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; ) func main() { now := time.Now() fmt.Println(\u0026#34;now is:\u0026#34;, now) fmt.Println(\u0026#34;one second later is:\u0026#34;, now.Add(time.Second)) fmt.Println(\u0026#34;one minute later is:\u0026#34;, now.Add(time.Minute)) fmt.Println(\u0026#34;one hour later is:\u0026#34;, now.Add(time.Hour)) d, err := time.ParseDuration(\u0026#34;3m20s\u0026#34;) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;3 minutes and 20 seconds later is:\u0026#34;, now.Add(d)) d, err = time.ParseDuration(\u0026#34;2h30m\u0026#34;) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;2 hours and 30 minutes later is:\u0026#34;, now.Add(d)) fmt.Println(\u0026#34;3 days and 2 hours later is:\u0026#34;, now.AddDate(0, 0, 3).Add(time.Hour*2)) } 需要注意的是，时间操作都是返回一个新的对象，原对象不会修改。carbon库也是如此。Go 的标准库也建议我们不要使用time.Time的指针。 当然carbon库也能使用上面的方法，它还提供了多种粒度的方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/uniplaces/carbon\u0026#34; ) func main() { now := carbon.Now() fmt.Println(\u0026#34;now is:\u0026#34;, now) fmt.Println(\u0026#34;one second later is:\u0026#34;, now.AddSecond()) fmt.Println(\u0026#34;one minute later is:\u0026#34;, now.AddMinute()) fmt.Println(\u0026#34;one hour later is:\u0026#34;, now.AddHour()) fmt.Println(\u0026#34;3 minutes and 20 seconds later is:\u0026#34;, now.AddMinutes(3).AddSeconds(20)) fmt.Println(\u0026#34;2 hours and 30 minutes later is:\u0026#34;, now.AddHours(2).AddMinutes(30)) fmt.Println(\u0026#34;3 days and 2 hours later is:\u0026#34;, now.AddDays(3).AddHours(2)) } carbon还提供了：\n增加季度的方法：AddQuarters/AddQuarter，复数形式介绍一个表示倍数的参数，单数形式倍数为1； 增加世纪的方法：AddCenturies/AddCentury； 增加工作日的方法：AddWeekdays/AddWeekday，这个方法会跳过非工作日； 增加周的方法：AddWeeks/AddWeek。 其实给上面方法传入负数就表示减少，另外carbon也提供了对应的Sub*方法。\n时间比较 注意：时间比较的是快慢。\n标准库time可以使用time.Time对象的Before/After/Equal判断是否在另一个时间对象前，后，或相等。carbon库也可以使用上面的方法比较时间。除此之外，它还提供了多组方法，每个方法提供一个简短名，一个详细名：\nEq/EqualTo：是否相等；不同时区的日期时间不同，但时间可能会相等。如 6:00 +0200 and 4:00 UTC are Equal. Ne/NotEqualTo：是否不等； Gt/GreaterThan：是否在之后； Gte/GreaterThanOrEqualTo:快或者相等。 Lt/LessThan：是否在之前； Lte/LessThanOrEqualTo：是否相同或在之前； Between：是否在两个时间之间。 另外carbon提供了：\n判断当前时间是周几的方法：IsMonday/IsTuesday/.../IsSunday； 是否是工作日，周末，闰年，过去时间还是未来时间：IsWeekday/IsWeekend/IsLeapYear/IsPast/IsFuture。 1 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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/uniplaces/carbon\u0026#34; ) func main() { t1, _ := carbon.CreateFromDate(2010, 10, 1, \u0026#34;Asia/Shanghai\u0026#34;) t2, _ := carbon.CreateFromDate(2011, 10, 20, \u0026#34;Asia/Shanghai\u0026#34;) fmt.Printf(\u0026#34;t1 equal to t2: %t\\n\u0026#34;, t1.Eq(t2)) fmt.Printf(\u0026#34;t1 not equal to t2: %t\\n\u0026#34;, t1.Ne(t2)) fmt.Printf(\u0026#34;t1 greater than t2: %t\\n\u0026#34;, t1.Gt(t2)) fmt.Printf(\u0026#34;t1 less than t2: %t\\n\u0026#34;, t1.Lt(t2)) fmt.Printf(\u0026#34;t1 greater than or equal: %t\u0026#34;, t1.Gte(t2)) t3, _ := carbon.CreateFromDate(2011, 1, 20, \u0026#34;Asia/Shanghai\u0026#34;) fmt.Printf(\u0026#34;t3 between t1 and t2: %t\\n\u0026#34;, t3.Between(t1, t2, true)) now := carbon.Now() fmt.Printf(\u0026#34;Weekday? %t\\n\u0026#34;, now.IsWeekday()) fmt.Printf(\u0026#34;Weekend? %t\\n\u0026#34;, now.IsWeekend()) fmt.Printf(\u0026#34;LeapYear? %t\\n\u0026#34;, now.IsLeapYear()) fmt.Printf(\u0026#34;Past? %t\\n\u0026#34;, now.IsPast()) fmt.Printf(\u0026#34;Future? %t\\n\u0026#34;, now.IsFuture()) } 时间差 我们还可以使用carbon计算两个日期之间相差多少秒、分、小时、天。\n用到的方法是func (c *Carbon) DiffInXXX(carb *Carbon, abs bool),表示的是相对于对象c快了还慢了多少XXX。正数表示c慢了，负数表示c快了。原理：\n1 diff := carb.Timestamp() - c.Timestamp() abs表示是否启用绝对值。\n示例如下：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/uniplaces/carbon\u0026#34; ) func main() { cst, _ := carbon.Today(\u0026#34;Asia/Shanghai\u0026#34;) hongKong, _ := carbon.Today(\u0026#34;Hongkong\u0026#34;) Japan, _ := carbon.Today(\u0026#34;Japan\u0026#34;) fmt.Println(cst.DiffInSeconds(hongKong, true)) // 0 fmt.Println(cst.DiffInHours(Japan, false)) // 0 Beijing, _ := carbon.CreateFromDate(2000, 1, 1, \u0026#34;Asia/Shanghai\u0026#34;) Tokyo, _ := carbon.CreateFromDate(2000, 1, 1, \u0026#34;Japan\u0026#34;) fmt.Println(Beijing.DiffInHours(Tokyo, true)) // 1 fmt.Println(Beijing.DiffInHours(Tokyo, false)) // -1 fmt.Println(Tokyo.DiffInHours(Beijing, false)) // 1 t, _ := carbon.CreateFromDate(2012, 1, 1, \u0026#34;UTC\u0026#34;) fmt.Println(t.DiffInDays(t.Copy().AddMonth(), false)) // 31 fmt.Println(t.DiffInDays(t.Copy().SubMonth(), false)) // -31 t, _ = carbon.CreateFromDate(2012, 4, 30, \u0026#34;UTC\u0026#34;) fmt.Println(t.DiffInDays(t.Copy().AddMonth(), false)) // 30 fmt.Println(t.DiffInDays(t.Copy().AddWeek(), false)) // 7 t, _ = carbon.CreateFromTime(10, 1, 1, 0, \u0026#34;UTC\u0026#34;) fmt.Println(t.DiffInMinutes(t.Copy().AddSeconds(59), true)) // 0 fmt.Println(t.DiffInMinutes(t.Copy().AddSeconds(60), true)) // 1 fmt.Println(t.DiffInMinutes(t.Copy().AddSeconds(119), true)) // 1 fmt.Println(t.DiffInMinutes(t.Copy().AddSeconds(120), true)) // 2 fmt.Println(t.DiffInHours(t.Copy().AddMinutes(59), false)) // 0 fmt.Println(t.DiffInHours(t.Copy().AddMinutes(60), false)) // 1 fmt.Println(t.DiffInHours(t.Copy().AddHours(2).AddMinutes(60), false)) // 3 } 注意：\n上面输出完全正确。 不理解是因为： 正数表示c慢了，负数表示c快了。 当北京时间和东京时间数字相同时，其实北京时间要快一个小时。即相同数字的北京时间和东京时间，北京时间的时间戳要大一点。因为实际北京时间比东京时间慢一个小时。 格式化 我们知道time.Time提供了一个Format方法，相比于其他编程语言使用格式化符来描述格式（需要记忆%d/%m/%h等的含义），Go 提供了一个一种更简单、直观的方式——使用 layout。即我们传入一个日期字符串，表示我们想要格式化成什么样子。Go 会用当前的时间替换字符串中的对应部分：\n1 2 3 4 5 6 7 8 9 10 11 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { t := time.Now() fmt.Println(t.Format(\u0026#34;2006-01-02 15:04:05\u0026#34;)) } 上面我们只需要传入一个2006-01-02 15:04:05表示我们想要的格式为yyyy-mm-dd hh:mm:ss，省去了我们需要记忆的麻烦。\n为了使用方便，Go 内置了一些标准的时间格式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // time/format.go const ( Layout = \u0026#34;01/02 03:04:05PM \u0026#39;06 -0700\u0026#34; // The reference time, in numerical order. ANSIC = \u0026#34;Mon Jan _2 15:04:05 2006\u0026#34; UnixDate = \u0026#34;Mon Jan _2 15:04:05 MST 2006\u0026#34; RubyDate = \u0026#34;Mon Jan 02 15:04:05 -0700 2006\u0026#34; RFC822 = \u0026#34;02 Jan 06 15:04 MST\u0026#34; RFC822Z = \u0026#34;02 Jan 06 15:04 -0700\u0026#34; // RFC822 with numeric zone RFC850 = \u0026#34;Monday, 02-Jan-06 15:04:05 MST\u0026#34; RFC1123 = \u0026#34;Mon, 02 Jan 2006 15:04:05 MST\u0026#34; RFC1123Z = \u0026#34;Mon, 02 Jan 2006 15:04:05 -0700\u0026#34; // RFC1123 with numeric zone RFC3339 = \u0026#34;2006-01-02T15:04:05Z07:00\u0026#34; RFC3339Nano = \u0026#34;2006-01-02T15:04:05.999999999Z07:00\u0026#34; Kitchen = \u0026#34;3:04PM\u0026#34; // Handy time stamps. Stamp = \u0026#34;Jan _2 15:04:05\u0026#34; StampMilli = \u0026#34;Jan _2 15:04:05.000\u0026#34; StampMicro = \u0026#34;Jan _2 15:04:05.000000\u0026#34; StampNano = \u0026#34;Jan _2 15:04:05.000000000\u0026#34; DateTime = \u0026#34;2006-01-02 15:04:05\u0026#34; DateOnly = \u0026#34;2006-01-02\u0026#34; TimeOnly = \u0026#34;15:04:05\u0026#34; ) 后面三个是在后面版本加上进的。\n除了上面这些格式，carbon还提供了其他一些格式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // github.com/uniplaces/carbon const ( DefaultFormat = \u0026#34;2006-01-02 15:04:05\u0026#34; DateFormat = \u0026#34;2006-01-02\u0026#34; FormattedDateFormat = \u0026#34;Jan 2, 2006\u0026#34; TimeFormat = \u0026#34;15:04:05\u0026#34; HourMinuteFormat = \u0026#34;15:04\u0026#34; HourFormat = \u0026#34;15\u0026#34; DayDateTimeFormat = \u0026#34;Mon, Jan 2, 2006 3:04 PM\u0026#34; CookieFormat = \u0026#34;Monday, 02-Jan-2006 15:04:05 MST\u0026#34; RFC822Format = \u0026#34;Mon, 02 Jan 06 15:04:05 -0700\u0026#34; RFC1036Format = \u0026#34;Mon, 02 Jan 06 15:04:05 -0700\u0026#34; RFC2822Format = \u0026#34;Mon, 02 Jan 2006 15:04:05 -0700\u0026#34; RFC3339Format = \u0026#34;2006-01-02T15:04:05-07:00\u0026#34; RSSFormat = \u0026#34;Mon, 02 Jan 2006 15:04:05 -0700\u0026#34; ) 注意一点，time库默认使用2006-01-02 15:04:05.999999999 -0700 MST格式，有点复杂了，carbon库默认使用更简洁的2006-01-02 15:04:05。\n使用只需要调func (t Time) Format(layout string)方法，layout为上面提供的格式化字符串。\ncarbon为了进一步方便使用，都将Fomat方法封装到了指定XxxString方法中.\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/uniplaces/carbon\u0026#34; ) func main() { now := carbon.Now() fmt.Println(now.Format(time.DateTime)) fmt.Println(now.Format(carbon.DefaultFormat), \u0026#34;=\u0026#34;, now.DateTimeString()) fmt.Println(now.Format(time.RFC3339)) fmt.Println(now.Format(carbon.RFC3339Format), \u0026#34;=\u0026#34;, now.RFC3339String()) fmt.Println(now.Format(carbon.DateFormat), \u0026#34;=\u0026#34;, now.DateString()) } 运行输出：\n1 2 3 4 5 6 $ go run main.go 2024-05-14 11:23:01 2024-05-14 11:23:01 = 2024-05-14 11:23:01 2024-05-14T11:23:01+08:00 2024-05-14T11:23:01+08:00 = 2024-05-14T11:23:01+08:00 2024-05-14 = 2024-05-14 高级特性 修饰器 Boundary：边界 所谓修饰器（modifier）就是对一些特定的时间操作，获取开始和结束时间。如当天、月、季度、年、十年、世纪、周的开始和结束时间，还能获得上一个周二、下一个周一、下一个工作日的时间等等：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/uniplaces/carbon\u0026#34; ) func main() { t := carbon.Now() fmt.Printf(\u0026#34;Start of day:%s\\n\u0026#34;, t.StartOfDay()) fmt.Printf(\u0026#34;End of day:%s\\n\u0026#34;, t.EndOfDay()) fmt.Printf(\u0026#34;Start of month:%s\\n\u0026#34;, t.StartOfMonth()) fmt.Printf(\u0026#34;End of month:%s\\n\u0026#34;, t.EndOfMonth()) fmt.Printf(\u0026#34;Start of year:%s\\n\u0026#34;, t.StartOfYear()) fmt.Printf(\u0026#34;End of year:%s\\n\u0026#34;, t.EndOfYear()) fmt.Printf(\u0026#34;Start of decade:%s\\n\u0026#34;, t.StartOfDecade()) fmt.Printf(\u0026#34;End of decade:%s\\n\u0026#34;, t.EndOfDecade()) fmt.Printf(\u0026#34;Start of century:%s\\n\u0026#34;, t.StartOfCentury()) fmt.Printf(\u0026#34;End of century:%s\\n\u0026#34;, t.EndOfCentury()) fmt.Printf(\u0026#34;Start of week:%s\\n\u0026#34;, t.StartOfWeek()) fmt.Printf(\u0026#34;End of week:%s\\n\u0026#34;, t.EndOfWeek()) fmt.Printf(\u0026#34;Next:%s\\n\u0026#34;, t.Next(time.Wednesday)) fmt.Printf(\u0026#34;Previous:%s\\n\u0026#34;, t.Previous(time.Wednesday)) } 自定义工作日和周末 有些地区每周的开始、周末和我们的不一样。Carbon默认是\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func NewCarbon(t time.Time) *Carbon { wds := []time.Weekday{ time.Saturday, time.Sunday, } return \u0026amp;Carbon{ Time: t, weekStartsAt: time.Monday, weekEndsAt: time.Sunday, weekendDays: wds, stringFormat: DefaultFormat, Translator: translator(), } } 例如，在美国周日是新的一周开始。没关系，carbon可以自定义每周的开始和周末：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/uniplaces/carbon\u0026#34; ) func main() { t, err := carbon.Create(2020, 02, 11, 0, 0, 0, 0, \u0026#34;Asia/Shanghai\u0026#34;) if err != nil { log.Fatal(err) } t.SetWeekStartsAt(time.Sunday) t.SetWeekEndsAt(time.Saturday) t.SetWeekendDays([]time.Weekday{time.Monday, time.Tuesday, time.Wednesday}) fmt.Printf(\u0026#34;Today is %s, weekend? %t\\n\u0026#34;, t.Weekday(), t.IsWeekend()) } 这个库一般默认就行。\n批量生成日期 利用Period方法，可以批量生成日期，接收三个参数。\n第一个：开始的时间。 第二个：日期间隔。 第三个：结束的时间。 返回值是一个时间对象切片。 效果为：从开始时间每隔多少天创建一个时间对象，直到结束日期。\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/uniplaces/carbon\u0026#34; ) func main() { t1, _ := carbon.Create(2012, 1, 1, 12, 0, 0, 0, \u0026#34;UTC\u0026#34;) t2, _ := carbon.Create(2012, 1, 31, 12, 0, 0, 0, \u0026#34;UTC\u0026#34;) days := 7 periods, err := carbon.Period(t1, days, t2) if err != nil { return } for _, val := range periods { fmt.Println(val) } } 运行输出：\n1 2 3 4 5 6 $ go run main.go 2012-01-01 12:00:00 2012-01-08 12:00:00 2012-01-15 12:00:00 2012-01-22 12:00:00 2012-01-29 12:00:00 （不常用）\n增强版介绍 下载：\n1 2 # 增强版 $ go get -u github.com/golang-module/carbon/v2 增强版创建对象 创建时间对象与低配版不一样。\n增强版的Now函数，参数是时区，为可选参数，默认为本地时区。增强版的时区，没有指定，一律为本地时区。\n低配版直接通过NewCarbon指定时间创建时间对象，而高配版还需要调CreateXxx方法指定时间才能完成创建。\n并且低配版如果只创建日期，那么时间为当前时间。而高配版只创建日期，时间为0:0:0.\n当只创建时间时，二者的日期都是为当前日期。 高配版移除了Create方法，创建了更多CreateXxx方法。用于指定时间或日期创建对象。\n对Parse方法进行了增强，并且还提供了另外两个方法，进行创建对象。这几个方法的时区都可选的，默认为本地时区。\n增强版Parse方法只能指定要解析的字符串和时区（可选）创建对象。如果没有指定时区，默认本地时区。时间布局任意。底层会遍历所有支持的布局模板（增强版叫Layout）。\nParseByLayout方法在Parse方法的基础上增加了一个布局模板参数。\nParseByFormat方法在Parse方法的基础上增加了一个格式模版参数。\n布局模板以Layout结尾，就是原来的Format方法的参数。\n1 DateLayout = \u0026#34;2006-01-02\u0026#34; 格式模版以format结尾，增强版新加的，由日期时间的格式化字符组成的字符串\n1 DateTimeFormat = \u0026#34;Y-m-d H:i:s\u0026#34; 注意carbon包的内部错误，都封装到了时间对象的Error属性下。\n如果有多个错误发生，只返回第一个错误，前一个错误排除后才返回下一个错误\n1 2 3 4 5 6 7 c := carbon.SetTimezone(\u0026#34;xxx\u0026#34;).Parse(\u0026#34;2020-08-05\u0026#34;) if c.Error != nil { // 错误处理 log.Fatal(c.Error) } // 输出 invalid timezone \u0026#34;xxx\u0026#34;, please see the file \u0026#34;$GOROOT/lib/time/zoneinfo.zip\u0026#34; for all valid timezones 实例如下：\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 45 46 47 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/golang-module/carbon/v2\u0026#34; ) func main() { now := carbon.Now() now = carbon.NewCarbon().Now() fmt.Println(now) // stdTime := carbon.CreateFromStdTime(time.Now()) fmt.Println(stdTime) // fromDate := carbon.CreateFromDate(2024, 5, 14) fromDate = carbon.NewCarbon().CreateFromDate(2024, 5, 14) fmt.Println(fromDate) // 2024-05-14 00:00:00 dateTime := carbon.CreateFromDateTime(2024, 5, 14, 10, 22, 33) dateTime = carbon.NewCarbon().CreateFromDateTime(2024, 5, 14, 10, 22, 33) fmt.Println(dateTime) // 2024-05-14 10:22:33 assertErr := func(carbon carbon.Carbon) { if carbon.Error != nil { log.Fatal(carbon.Error) } } parse := carbon.Parse(\u0026#34;2024-05-20 10:22:33\u0026#34;) assertErr(parse) fmt.Println(parse) // 2024-05-20 10:22:33 parse = carbon.ParseByLayout(\u0026#34;2024-05-20\u0026#34;, carbon.DateLayout) assertErr(parse) fmt.Println(parse) // 2024-05-20 00:00:00 parse = carbon.ParseByFormat(\u0026#34;2024-05-20 10:22:33\u0026#34;, carbon.DateTimeFormat) assertErr(parse) fmt.Println(parse) // 2024-05-20 10:22:33 toString := carbon.CreateFromTimestamp(-1) fmt.Println(toString) // 1970-01-01 07:59:59 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // Create a Carbon instance from a given hour, minute and second carbon.CreateFromTime(13, 14, 15).ToString() // 2020-08-05 13:14:15 +0800 CST // Create a Carbon instance from a given hour, minute and second with millisecond carbon.CreateFromTimeMilli(13, 14, 15, 999).ToString() // 2020-08-05 13:14:15.999 +0800 CST // Create a Carbon instance from a given hour, minute and second with microsecond carbon.CreateFromTimeMicro(13, 14, 15, 999999).ToString() // 2020-08-05 13:14:15.999999 +0800 CST // Create a Carbon instance from a given hour, minute and second with nanosecond carbon.CreateFromTimeNano(13, 14, 15, 999999999).ToString() // 2020-08-05 13:14:15.999999999 +0800 CST carbon.Parse(\u0026#34;now\u0026#34;).ToString() // 2020-08-05 13:14:15 +0800 CST carbon.Parse(\u0026#34;yesterday\u0026#34;).ToString() // 2020-08-04 13:14:15 +0800 CST carbon.Parse(\u0026#34;tomorrow\u0026#34;).ToString() // 2020-08-06 13:14:15 +0800 CST carbon.Parse(\u0026#34;2020\u0026#34;).ToString() // 2020-01-01 00:00:00 +0800 CST carbon.Parse(\u0026#34;2020-8\u0026#34;).ToString() // 2020-08-01 00:00:00 +0800 CST carbon.ParseByFormat(\u0026#34;2020|08|05 13|14|15\u0026#34;, \u0026#34;Y|m|d H|i|s\u0026#34;).ToDateTimeString() // 2020-08-05 13:14:15 carbon.ParseByFormat(\u0026#34;It is 2020-08-05 13:14:15\u0026#34;, \u0026#34;\\\\I\\\\t \\\\i\\\\s Y-m-d H:i:s\u0026#34;).ToDateTimeString() // 2020-08-05 13:14:15 carbon.ParseByFormat(\u0026#34;今天是 2020年08月05日13时14分15秒\u0026#34;, \u0026#34;今天是 Y年m月d日H时i分s秒\u0026#34;).ToDateTimeString() // 2020-08-05 13:14:15 carbon.ParseByLayout(\u0026#34;2020|08|05 13|14|15\u0026#34;, \u0026#34;2006|01|02 15|04|05\u0026#34;).ToDateTimeString() // 2020-08-05 13:14:15 carbon.ParseByLayout(\u0026#34;It is 2020-08-05 13:14:15\u0026#34;, \u0026#34;It is 2006-01-02 15:04:05\u0026#34;).ToDateTimeString() // 2020-08-05 13:14:15 carbon.ParseByLayout(\u0026#34;今天是 2020年08月05日13时14分15秒\u0026#34;, \u0026#34;今天是 2006年01月02日15时04分05秒\u0026#34;).ToDateTimeString() // 2020-08-05 13:14:15 一些默认值 增强版创建的时间对象的默认值：\n时间格式与低配版一直，名字改成carbon.DateTimeLayout。 时区默认都为本地时区。 工作日开始是在Sunday，低配版是在星期一。 默认的语言区域是en。支持的语言在github.com\\golang-module\\carbon\\lang目录下。 这些属性可以通过func SetDefault(d Default)方法修改。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/golang-module/carbon/v2\u0026#34; ) func main() { now := carbon.Now() fmt.Println(now) carbon.SetDefault(carbon.Default{Layout: carbon.RFC3339Layout, Timezone: carbon.Local, WeekStartsAt: carbon.Monday, Locale: \u0026#34;zh-CN\u0026#34;}) fmt.Println(now) } 官方建议在main.go等入口文件中修改默认值。\n对象互转 增强版可以实现time.Time对象与Carbon对象相互转换。\n示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/golang-module/carbon/v2\u0026#34; ) func main() { carbonTime := carbon.CreateFromTime(22, 22, 22) stdTime := carbonTime.StdTime() fmt.Println(stdTime) // 2024-05-14 22:22:22 +0800 CST stdTime = time.Date(2024, 5, 14, 22, 22, 22, 0, time.Local) carbonTime = carbon.CreateFromStdTime(stdTime) fmt.Println(carbonTime) // 2024-05-14 22:22:22 } 更多细节的增强 对象转字符串 增强版提供了更多时间对象转字符串的方法。除了String方法外，添加了ToXxxString方法：\n1 2 3 4 5 6 7 8 9 carbon.Now().String() // 2020-08-05 13:14:15 carbon.Now().ToString() // 2020-08-05 13:14:15 +0800 CST carbon.Now().ToDateTimeString() // 2020-08-05 13:14:15 // Return date of today carbon.Now().ToDateString() // 2020-08-05 // Return time of today carbon.Now().ToTimeString() // 13:14:15 // Return datetime of today in a given timezone carbon.Now(Carbon.NewYork).ToDateTimeString() // 2020-08-05 14:14:15 注意：增强版有一个坑：当我们直接输出Carbon对象时，会自动调用String方法，这个String方法会将时区改成本地。若不希望修改时区，请不要调用String方法，用别的转字符串的方法，如ToDateTimeString。\n更多内容参考时间输出\n对象转时间戳 通过增强版，我们可以方便的将Carbon对象转换成指定单位的时间戳：\n1 2 3 4 5 6 7 8 9 carbon.Now(Carbon.NewYork).ToDateTimeString() // 2020-08-05 14:14:15 // Return timestamp with second of today carbon.Now().Timestamp() // 1596604455 // Return timestamp with millisecond of today carbon.Now().TimestampMilli() // 1596604455999 // Return timestamp with microsecond of today carbon.Now().TimestampMicro() // 1596604455999999 // Return timestamp with nanosecond of today carbon.Now().TimestampNano() // 1596604455999999999 获取昨天、明天的对象 增强版，不仅能够获取当前的时间对象，还能获取昨天或者明天的时间：\n1 2 3 4 5 6 7 8 fmt.Printf(\u0026#34;%s\u0026#34;, carbon.Yesterday()) // 2020-08-04 13:14:15 carbon.Yesterday().String() // 2020-08-04 13:14:15 carbon.Yesterday().ToString() // 2020-08-04 13:14:15 +0800 CST carbon.Yesterday(Carbon.NewYork).ToDateTimeString() // 2020-08-04 13:14:15 carbon.Tomorrow().String() // 2020-08-06 13:14:15 carbon.Tomorrow().ToString() // 2020-08-06 13:14:15 +0800 CST carbon.Tomorrow().ToDateTimeString() // 2020-08-06 13:14:15 增强时间戳创建对象 低配版只能通过CreateFromTimestamp和CreateFromTimestampUTC函数创建对象，增强版提供了毫秒、微秒、纳秒时间戳创建对象。时区默认本地时区。\n1 2 3 4 5 6 7 8 carbon.CreateFromTimestamp(1649735755).ToString() // 2022-04-12 11:55:55 +0800 CST // Create a Carbon instance from a given timestamp with millisecond carbon.CreateFromTimestampMilli(1649735755981).ToString() // 2022-04-12 11:55:55.981 +0800 CST // Create a Carbon instance from a given timestamp with microsecond carbon.CreateFromTimestampMicro(1649735755981566).ToString() // 2022-04-12 11:55:55.981566 +0800 CST // Create a Carbon instance from a given timestamp with nanosecond carbon.CreateFromTimestampNano(1649735755981566000).ToString() // 2022-04-12 11:55:55.981566 +0800 CST carbon.CreateFromTimestampNano(1649735755981566000, \u0026#34;Japan\u0026#34;).ToString() // 2022-04-12 12:55:55.981566 +0900 JST 快一个小时。 其他创建对象增强\n时间的边界 与低配版一模一样。详情请看修饰器。\n增强时间运算 支持链式调用，对应时间的运算，增加了不会溢出的方法。\n默认低配版和高配版进行时间运算时，当为2月29的时候，即闰年的时候，计算到不是闰年，都没有29号，那么时间默认会溢出，变成3月1号。增强版提供了AddXxxNoOverflow、SubXxxNoOverflow方法（低配版部分有，高配版基本都有），让时间不会溢出，为2月28。（闰年比公历年多1天。）\n增强版还增强了别的单位进行运算,如XxxDecades十年、XxxDuration根据片段进行运算。还更小单位的运算，如SubMillisecond、SubNanossecond\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 三个年代后 carbon.Parse(\u0026#34;2020-02-29 13:14:15\u0026#34;).AddDecades(3).ToDateTimeString() // 2050-03-01 13:14:15 // 三个年代后(月份不溢出) carbon.Parse(\u0026#34;2020-02-29 13:14:15\u0026#34;).AddDecadesNoOverflow(3).ToDateTimeString() // 2050-02-28 13:14:15 // 一个年代后 carbon.Parse(\u0026#34;2020-02-29 13:14:15\u0026#34;).AddDecade().ToDateTimeString() // 2030-03-01 13:14:15 // 一个年代后(月份不溢出) carbon.Parse(\u0026#34;2020-02-29 13:14:15\u0026#34;).AddDecadeNoOverflow().ToDateTimeString() // 2030-02-28 13:14:15 // 二小时半前 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).SubDuration(\u0026#34;2.5h\u0026#34;).ToDateTimeString() // 2020-08-05 10:44:15 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).SubDuration(\u0026#34;2h30m\u0026#34;).ToDateTimeString() // 2020-08-05 10:44:15 // 三微秒后 carbon.Parse(\u0026#34;2020-08-05 13:14:15.222222222\u0026#34;).AddMicroseconds(3).ToString() // 2020-08-05 13:14:15.222225222 +0800 CST // 一微秒后 carbon.Parse(\u0026#34;2020-08-05 13:14:15.222222222\u0026#34;).AddMicrosecond().ToString() // 2020-08-05 13:14:15.222223222 +0800 CST 其他的与低配版一直。\n增强时间差 增强版移除了是否取绝对值参数，封装到了DiffAbsInXxx方法中。 原理差不多，后面-前面。快为负数，慢为正数（相对调用的时间对象）。 其他相差多少分、小时、年、与低配版时间差一直 新增时间差的字符串表示，用DiffInString方法获取，默认参数为当前时间。 同样提供了绝对值方法DiffAbsInString 需要注意的是：目前时间差的字符串表示只能表示差里面的最大时间单位。 如差2分30秒，结果为差2分钟。差3月22天，结果为差3个月 该方法支持国际化。 新增相差时长（片段）字符串表示。用DiffInDuration方法获取，默认参数为当前时间。 同样提供了绝对值方法DiffAbsInDuration 需要注意的是：这个方法会具体到相差多少时差。当最大单位为小时。 该方法同样支持国际化。 新增对人类友好的可读格式时间差。用DiffForHumans方法获取，默认参数为当前时间。 该方法人类可读，移除了绝对值方法。能够直观的看见参数时间对象是前（负数）还是后（正数）。 需要注意的是该方法，与DiffInString一样，只能表示最大的时间单位。 同样支持国际化。 示例如下：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/golang-module/carbon/v2\u0026#34; ) func main() { carbon.SetDefault(carbon.Default{ Layout: carbon.DateTimeLayout, Timezone: carbon.Local, WeekStartsAt: carbon.Monday, Locale: \u0026#34;zh-CN\u0026#34;, }) inString := carbon.Now().DiffInString(carbon.Now().AddDays(22).AddMinutes(22)) fmt.Println(inString) // 3 周 diffInString := carbon.Now().AddHours(22).AddMinutes(22).DiffInString() fmt.Println(diffInString) // -22 小时 diffAbsInDuration := carbon.Now().AddYears(2).AddMinutes(22).AddMicroseconds(22).DiffAbsInDuration() fmt.Println(diffAbsInDuration) // 17520h22m0.000022s inString = carbon.Now().DiffForHumans(carbon.Now().AddDays(22).AddMinutes(22)) fmt.Println(inString) // 3 周前 diffInString = carbon.Now().AddHours(22).AddMinutes(22).DiffForHumans() fmt.Println(diffInString) // 22 小时后 } 获取时间极值 低配版只提供了Closest和Farthest获取距对象最近或最远的时间对象，低配版和高配版都只能传递两个时间对象。\n高配版增强版新增两个方法Max和``Min`，获取多个时间里面的最大或最小，参数为至少一个时间对象。\n示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 c0 := carbon.Parse(\u0026#34;2023-04-01\u0026#34;) c1 := carbon.Parse(\u0026#34;2023-03-28\u0026#34;) c2 := carbon.Parse(\u0026#34;2023-04-16\u0026#34;) // 返回最近的 Carbon 实例 c0.Closest(c1, c2) // c1 // 返回最远的 Carbon 实例 c0.Farthest(c1, c2) // c2 yesterday := carbon.Yesterday() today := carbon.Now() tomorrow := carbon.Tomorrow() // 返回最大的 Carbon 实例 carbon.Max(yesterday, today, tomorrow) // tomorrow // 返回最小的 Carbon 实例 carbon.Min(yesterday, today, tomorrow) // yesterday 增强时间判断 增强版新增判断时间是否有效：IsValid、IsInvalid\n原理是时间戳大于0，就有效。当为0或者为空或者Error属性不为nil时，该时间无效。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 是否是有效时间 func (c Carbon) IsValid() bool { if c.Error != nil { return false } if c.time.IsZero() { return false } // 大于零值时间 if c.StdTime().Unix() \u0026gt; -62135596800 { return true } return false } 新增更多的时间判断：\n判断是否是早上、下午、当前、未来、过去、闰年、长年、几月、星期几、工作日、周末、昨天、今天、明天、同一世纪、同一年代、同一年、同一季节、同一月、同一天、同一小时、同一分钟、同一秒。方法名是IsXxx。 需要注意的是：增强版移除了自定义周末。周末默认都为星期6、星期天。只保留了设置一周开始的日期。\n新增星座判断，方法为IsXxx\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 // 获取星座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).Constellation() // Leo // 是否是白羊座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsAries() // false // 是否是金牛座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsTaurus() // false // 是否是双子座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsGemini() // false // 是否是巨蟹座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsCancer() // false // 是否是狮子座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsLeo() // true // 是否是处女座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsVirgo() // false // 是否是天秤座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsLibra() // false // 是否是天蝎座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsScorpio() // false // 是否是射手座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsSagittarius() // false // 是否是摩羯座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsCapricorn() // false // 是否是水瓶座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsAquarius() // false // 是否是双鱼座 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsPisces() // false 新增季节判断：按照气象划分，即3-5月为春季，6-8月为夏季，9-11月为秋季，12-2月为冬季\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 获取季节 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).Season() // Summer // 本季节开始时间 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).StartOfSeason().ToDateTimeString() // 2020-06-01 00:00:00 // 本季节结束时间 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).EndOfSeason().ToDateTimeString() // 2020-08-31 23:59:59 // 是否是春季 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsSpring() // false // 是否是夏季 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsSummer() // true // 是否是秋季 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsAutumn() // false // 是否是冬季 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).IsWinter() // false ​\n​\n增强时间比较 高配版保留了低配版的所有时间比较方法，同时新增判断是否在两个时间之间，包括两端的时间方法和比较方法Compare，该方法需要指定比较字符=,\u0026lt;=,!=,\u0026lt;\u0026gt;等（只能接收一个比较符，因为只能有一个时间参数比较）。示例如下：\n1 2 3 4 5 6 7 8 9 // 是否在两个时间之间(包括开始时间) carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).BetweenIncludedStart(carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;), carbon.Parse(\u0026#34;2020-08-06 13:14:15\u0026#34;)) // true // 是否在两个时间之间(包括结束时间) carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).BetweenIncludedEnd(carbon.Parse(\u0026#34;2020-08-04 13:14:15\u0026#34;), carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;)) // true // 是否在两个时间之间(包括这两个时间) carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).BetweenIncludedBoth(carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;), carbon.Parse(\u0026#34;2020-08-06 13:14:15\u0026#34;)) // true carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).Compare(\u0026#34;\u0026gt;=\u0026#34;, carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;)) // true carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).Compare(\u0026#34;\u0026lt;\u0026gt;\u0026#34;, carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;)) // false 就是不等于 增强时间设置 可以设置时区、设置地区：func SetTimezone(name string)、func SetLocation(loc *time.Location)、func LoadLocation(name string)、func getLocationByTimezone(timezone string)\n时区与地区同名。关系是：地区与时区可以相互转换。可以划等号。 只要是时区文件里面没有的名称时区和地区都不能使用。虽然增强版将时区文件中的时区名都封装成了常量，方便调用。 可以设置区域（国际化设置）：SetLocale\n还可以修改年月日时分秒，用SetXxx方法\n示例如下：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/golang-module/carbon/v2\u0026#34; ) func main() { Shanghai, _ := time.LoadLocation(carbon.Shanghai) Beijing := carbon.SetLocation(Shanghai).Now() BeijingStr := Beijing.ToDateTimeString() Japan := carbon.SetTimezone(carbon.Japan).Now() JapanStr := Japan.ToDateTimeString() fmt.Printf(\u0026#34;东京时间%s比北京时间%s快一个小时，但表示的是同一时刻\\n\u0026#34;, BeijingStr, JapanStr) Beijing = carbon.SetTimezone(carbon.HongKong).Now() BeijingStr = Beijing.ToDateTimeString() Tokyo, _ := time.LoadLocation(carbon.Tokyo) Japan = carbon.SetLocation(Tokyo).Now() JapanStr = Japan.ToDateTimeString() fmt.Printf(\u0026#34;东京时间%s比北京时间%s快一个小时，但表示的是同一时刻\\n\u0026#34;, BeijingStr, JapanStr) } 注意：增强版有一个坑：当我们直接输出Carbon对象时，会自动调用String方法，这个String方法会将时区改成本地。若不希望修改时区，请不要调用String方法，用别的转字符串的方法，如ToDateTimeString。\n1 2 3 4 5 6 7 // 设置年月日时分秒纳秒 carbon.Parse(\u0026#34;2020-01-01\u0026#34;).SetDateTimeNano(2019, 2, 2, 13, 14, 15, 999999999).ToString() // 2019-02-02 13:14:15.999999999 +0800 CST carbon.Parse(\u0026#34;2020-01-01\u0026#34;).SetDateTimeNano(2019, 2, 31, 13, 14, 15, 999999999).ToString() // 2019-03-03 13:14:15.999999999 +0800 CST // 单独设置纳秒 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).SetNanosecond(100000000).Nanosecond() // 100000000 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).SetNanosecond(999999999).Nanosecond() // 999999999 时间获取 一句话：直接调用你想获取的时间或日期，只要你想到的都能获取，如获取本年总天数DaysInYear、获取本月总天数DaysInMonth、获取本年第几天、 获取本周第几天：DayOfXxx、获取具体日期或时间DateTime、获取当前世纪Century、获取当前年代Decade十年未一个年代、年月日时分秒毫米微秒纳秒、时间戳就不说了。获取时区Timezone、获取位置Location、获取距离UTC时区的偏移量，单位秒Offset、获取当前区域Locale、 获取当前星座Constellation、获取当前季节Season\n获取年龄Age。（你就说强不强大~）\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 // 获取年龄 carbon.Parse(\u0026#34;2002-01-01 13:14:15\u0026#34;).Age() // 17 // 获取当前星座 carbon.Now().SetLocale(\u0026#34;en\u0026#34;).Constellation() // Leo carbon.Now().SetLocale(\u0026#34;zh-CN\u0026#34;).Constellation() // 狮子座 // 获取时区 carbon.SetTimezone(carbon.PRC).Timezone() // CST carbon.SetTimezone(carbon.Tokyo).Timezone() // JST // 获取位置 carbon.SetTimezone(carbon.PRC).Location() // PRC carbon.SetTimezone(carbon.Tokyo).Location() // Asia/Tokyo // 获取距离UTC时区的偏移量，单位秒 carbon.SetTimezone(carbon.PRC).Offset() // 28800 carbon.SetTimezone(carbon.Tokyo).Offset() // 32400 // 获取当前区域 carbon.Now().Locale() // en carbon.Now().SetLocale(\u0026#34;zh-CN\u0026#34;).Locale() // zh-CN // 获取本月第几天 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).DayOfMonth() // 5 // 获取本月第几周 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).WeekOfMonth() // 1 更多示例参考\n时间输出 时间输出是ToXxxString方法。（你能想到的格式都能输出😂）\n注意：增强版有一个坑：当我们直接输出Carbon对象时，会自动调用String方法，这个String方法会将时区改成本地。若不希望修改时区，请不要调用String方法，用别的转字符串的方法，如ToDateTimeString。\nToString方法为输出time包的默认格式。\n输出指定布局的字符串Layout\n输出指定格式的字符串(如果使用的字母与格式化字符冲突时，请使用\\符号转义该字符Format\n示例如下：\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 // 输出简写日期字符串 carbon.Parse(\u0026#34;2020-08-05 13:14:15.999999999\u0026#34;).ToShortDateString() // 20200805 // 输出简写时间字符串 carbon.Parse(\u0026#34;2020-08-05 13:14:15.999999999\u0026#34;).ToShortTimeString() // 131415 // 输出简写时间字符串，包含毫秒 carbon.Parse(\u0026#34;2020-08-05 13:14:15.999999999\u0026#34;).ToShortTimeMilliString() // 131415.999 // 输出简写时间字符串，包含微秒 carbon.Parse(\u0026#34;2020-08-05 13:14:15.999999999\u0026#34;).ToShortTimeMicroString() // 131415.999999 // 输出简写时间字符串，包含纳秒 carbon.Parse(\u0026#34;2020-08-05 13:14:15.999999999\u0026#34;).ToShortTimeNanoString() // 131415.999999999 // 输出 UnixDate 格式字符串 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).ToUnixDateString() // Wed Aug 5 13:14:15 CST 2020 // 输出 RFC3339 格式字符串 carbon.Parse(\u0026#34;2020-08-05T13:14:15.999999999+08:00\u0026#34;).ToRfc3339String() // 2020-08-05T13:14:15+08:00 // 输出\u0026#34;2006-01-02 15:04:05.999999999 -0700 MST\u0026#34;格式字符串 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).ToString() // 2020-08-05 13:14:15.999999 +0800 CST // 输出 \u0026#34;Jan 2, 2006\u0026#34; 格式字符串 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).ToFormattedDateString() // Aug 5, 2020 // 输出 \u0026#34;Mon, Jan 2, 2006\u0026#34; 格式字符串 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).ToFormattedDayDateString() // Wed, Aug 5, 2020 // 输出指定布局的字符串 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).Layout(carbon.ISO8601Layout) // 2020-08-05T13:14:15+08:00 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).Layout(\u0026#34;20060102150405\u0026#34;) // 20200805131415 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).Layout(\u0026#34;2006年01月02日 15时04分05秒\u0026#34;) // 2020年08月05日 13时14分15秒 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).Layout(\u0026#34;It is 2006-01-02 15:04:05\u0026#34;) // It is 2020-08-05 13:14:15 // 输出指定格式的字符串(如果使用的字母与格式化字符冲突时，请使用\\符号转义该字符) carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).Format(\u0026#34;YmdHis\u0026#34;) // 20200805131415 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).Format(\u0026#34;Y年m月d日 H时i分s秒\u0026#34;) // 2020年08月05日 13时14分15秒 carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).Format(\u0026#34;l jS \\\\o\\\\f F Y h:i:s A\u0026#34;) // Wednesday 5th of August 2020 01:14:15 PM carbon.Parse(\u0026#34;2020-08-05 13:14:15\u0026#34;).Format(\u0026#34;\\\\I\\\\t \\\\i\\\\s Y-m-d H:i:s\u0026#34;) // It is 2020-08-05 13:14:15 更多示例参考\n更多格式化输出符号请查看附录 格式化符号表\n输出结构体 时间输出不仅能够输出指定字符串ToXxxString，还能输出到指定的结构体类型ToXxxStruct，用与序列化与反序列化指定日期时间格式。只不过输出的结构体类型只有Date和Time组合的。\n示例如下：\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 package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/golang-module/carbon/v2\u0026#34; ) type Student struct { Birthday1 carbon.DateTime `json:\u0026#34;birthday1\u0026#34;` Birthday2 carbon.Date `json:\u0026#34;birthday2\u0026#34;` } func main() { s := Student{ Birthday1: carbon.Now().SubYears(5).ToDateTimeStruct(), Birthday2: carbon.Now().SubYears(10).ToDateStruct(), } marshal, err := json.Marshal(s) if err != nil { log.Fatal(err) } println(string(marshal)) // {\u0026#34;birthday1\u0026#34;:\u0026#34;2019-05-15 14:57:07\u0026#34;,\u0026#34;birthday2\u0026#34;:\u0026#34;2014-05-15\u0026#34;} s2 := new(Student) err = json.Unmarshal(marshal, s2) if err != nil { log.Fatal(err) } fmt.Println(s2.Birthday1) // 2019-05-15 15:00:27 fmt.Println(s2.Birthday2) // 2014-05-15 } 总结 carbon提供了很多的实用方法，另外time的方法它也能使用，使得它的功能非常强大。时间其实是一个非常复杂的问题，考虑到时区、闰秒、各地的夏令时等，自己处理起来简直是火葬场。幸好有这些库(┬＿┬)\n参考 carbon GitHub 仓库： https://github.com/uniplaces/carbon 增强版carbon GitHub 仓库： https://github.com/golang-module/carbon Go 每日一库之 carbon ","date":"2024-05-13T10:19:13+08:00","permalink":"https://arlettebrook.github.io/p/carbon-introduction/","title":"Carbon Introduction"},{"content":" 简介 twelve-factor应用提倡将配置存储在环境变量中。任何从开发环境切换到生产环境时需要修改的东西都从代码抽取到环境变量里。 但是在实际开发中，如果同一台机器运行多个项目，设置环境变量容易冲突，不实用。godotenv库从.env文件中读取配置， 然后存储到程序的环境变量中。在代码中可以使用读取非常方便。godotenv源于一个 Ruby 的开源项目dotenv。\n快速使用 第三方库需要先安装：\n1 $ go get -u github.com/joho/godotenv 后使用：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/joho/godotenv\u0026#34; ) func main() { err := godotenv.Load() if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;name：\u0026#34;, os.Getenv(\u0026#34;name\u0026#34;)) fmt.Println(\u0026#34;age：\u0026#34;, os.Getenv(\u0026#34;AGE\u0026#34;)) } 然后在可执行程序相同目录下，添加一个.env文件（可以给ide安装插件，检查.env文件语法，安装用的人多的。如GoLand：.env files support）：\n1 2 NAME=arlettebrook AGE=18 运行程序，输出：\n1 2 3 $ go run main.go name： arlettebrook age： 18 可见，使用非常方便。默认情况下，godotenv读取项目根目录下的.env文件，文件中使用key=value的格式，每行一个键值对。 调用godotenv.Load()即可加载，可直接调用os.Getenv(\u0026quot;key\u0026quot;)读取,os.Getenv是用来读取环境变量的：windows上不区分大小写，但环境变量通常都是大写，建议用大写。没找到返回空字符串。\n1 2 3 4 5 6 7 8 9 10 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func main() { fmt.Println(os.Getenv(\u0026#34;GOPATH\u0026#34;)) // 会返回GOPAHT环境变量的值 } 基本使用 自动加载 如果你有程序员的优良传统——懒，你可能连Load方法都不想自己调用。没关系，godotenv给你懒的权力！\n导入github.com/joho/godotenv/autoload，配置会自动读取：\n1 2 3 4 5 6 7 8 9 10 11 12 13 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; _ \u0026#34;github.com/joho/godotenv/autoload\u0026#34; ) func main() { fmt.Println(\u0026#34;name: \u0026#34;, os.Getenv(\u0026#34;NAME\u0026#34;)) fmt.Println(\u0026#34;age: \u0026#34;, os.Getenv(\u0026#34;AGE\u0026#34;)) } 注意，由于代码中没有显式用到godotenv库，需要使用空导入，即导入时包名前添加一个_。作用：自动调用init函数。\n看autoload包的源码，其实就是库帮你调用了Load方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package autoload /* You can just read the .env file on import just by doing import _ \u0026#34;github.com/joho/godotenv/autoload\u0026#34; And bob\u0026#39;s your mother\u0026#39;s brother */ import \u0026#34;github.com/joho/godotenv\u0026#34; func init() { godotenv.Load() } 仔细看注释，程序员的恶趣味😂！\n加载自定义文件 默认情况下，加载的是项目根目录下的.env文件。当然我们可以加载任意名称的文件，文件也不必以.env为后缀：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/joho/godotenv\u0026#34; ) func main() { err := godotenv.Load(\u0026#34;common\u0026#34;, \u0026#34;.env.production\u0026#34;, \u0026#34;.env.development\u0026#34;) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;name: \u0026#34;, os.Getenv(\u0026#34;NAME\u0026#34;)) fmt.Println(\u0026#34;version: \u0026#34;, os.Getenv(\u0026#34;VERSION\u0026#34;)) fmt.Println(\u0026#34;database: \u0026#34;, os.Getenv(\u0026#34;DATABASE\u0026#34;)) } common文件内容：\n1 2 NAME=awesome web VERSION=0.0.1 .env.development：\n1 DATABASE=sqlite .env.production：\n1 DATABASE=mysql 运行输出：\n1 2 3 4 $ go run main.go name: awesome web version: 0.0.1 database: mysql 注意事项：\nLoad接收多个文件名作为参数，如果不传入文件名，默认读取.env文件的内容。 当指定了环境变量文件，默认的.env文件会失效，除非你加进去。 如果多个文件中存在同一个键，那么先出现的优先，后出现的不生效。所以，上面输出的database是mysql。 原因：先出现的已经加载到环境变量中了，默认不会覆盖环境变量中的值。 使用Load方法加载的环境变量不会覆盖默认的环境变量，要覆盖请用Overload方法。 使用这个加载，上面输出的database是sqlite。不信你可以试试\u0026hellip; 以上两种方法都会对环境变量的副本，进行添加或修改。后面会介绍，不存入环境变量。 注释 .env文件中可以添加注释，注释以#开始，直到该行结束。\n1 2 3 4 # app name NAME=awesome web # current version VERSION=0.0.1 YAML .env文件还可以使用 YAML 格式：\n1 2 NAME: \u0026#39;awesome web\u0026#39; VERSION: 0.0.1 1 2 3 4 5 6 7 8 9 10 11 12 13 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; _ \u0026#34;github.com/joho/godotenv/autoload\u0026#34; ) func main() { fmt.Println(\u0026#34;name: \u0026#34;, os.Getenv(\u0026#34;NAME\u0026#34;)) fmt.Println(\u0026#34;version: \u0026#34;, os.Getenv(\u0026#34;VERSION\u0026#34;)) } 运行输出：\n1 2 3 $ go run main.go name: awesome web version: 0.0.1 注意：yaml格式不支持嵌套。官方解释：支持 YAML(ish) 风格。\n不存入环境变量 从文件读取 godotenv允许不将.env文件内容存入环境变量，使用godotenv.Read()返回一个map[string]string，可直接使用：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/joho/godotenv\u0026#34; ) func main() { myEnv, err := godotenv.Read() if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;name: \u0026#34;, myEnv[\u0026#34;NAME\u0026#34;]) fmt.Println(\u0026#34;version: \u0026#34;, myEnv[\u0026#34;VERSION\u0026#34;]) } 注意：\n环境配置文件中的键值对，会保存在返回的map[string]string中，键名与配置键必须同名。 直接操作map，简单直接！ 这样就不会将环境配置文件中的变量存入环境变量。 Read可以接收文件路径，用于指定配置文件。默认./.env,与Load一致。 从string, byte中读取配置 除了读取文件，还可以从string中读取配置，它不会修改环境：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/joho/godotenv\u0026#34; ) func main() { stringContent := ` name: awesome web version: 0.0.1 ` byteContent := []byte(` name: awesome web byte version: 1.0.1`) myEnvWithString, err := godotenv.Unmarshal(stringContent) myEnvWithByte, err := godotenv.UnmarshalBytes(byteContent) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;name: \u0026#34;, myEnvWithString[\u0026#34;name\u0026#34;]) fmt.Println(\u0026#34;version: \u0026#34;, myEnvWithString[\u0026#34;version\u0026#34;]) fmt.Println(\u0026#34;--------UnmarshalBytes--------\u0026#34;) fmt.Println(\u0026#34;name: \u0026#34;, myEnvWithByte[\u0026#34;name\u0026#34;]) fmt.Println(\u0026#34;version: \u0026#34;, myEnvWithByte[\u0026#34;version\u0026#34;]) } 通过Unmarshal方法，可以从字符串中读取env文件。存储在返回值map[string]string类型中。 通过UnmarshalBytes方法，可以从字节切片中读取env文件。也是存储在map中。 运行输出：\n1 2 3 4 5 6 $ go run main.go name: awesome web version: 0.0.1 --------UnmarshalBytes-------- name: awesome web byte version: 1.0.1 从io.Reader获取配置 除了以上方法外，还可以从io.Reader中读取env文件。这个也不会修改环境。\n只要实现了io.Reader接口，就能作为数据源。可以从文件（os.File），网络（net.Conn），bytes.Buffer等多种来源读取：\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 package main import ( \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/joho/godotenv\u0026#34; ) func main() { file, _ := os.Open(\u0026#34;.env\u0026#34;) myEnv, err := godotenv.Parse(file) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;name: \u0026#34;, myEnv[\u0026#34;NAME\u0026#34;]) fmt.Println(\u0026#34;version: \u0026#34;, myEnv[\u0026#34;VERSION\u0026#34;]) buf := bytes.NewBuffer([]byte{}) buf.WriteString(\u0026#34;name: awesome web @buffer\u0026#34;) buf.Write([]byte{\u0026#39;\\n\u0026#39;}) buf.WriteString(\u0026#34;version: 0.0.1 @buffer\u0026#34;) myEnv, err = godotenv.Parse(buf) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;name: \u0026#34;, myEnv[\u0026#34;name\u0026#34;]) fmt.Println(\u0026#34;version: \u0026#34;, myEnv[\u0026#34;version\u0026#34;]) } 通过Parse方法，可以中io中读取env文件。从字符串中读取是Unmarshal方法，二者不一样。 读取的配置都保存在map中，没有存入环境变量。map键与配置键必须同名。通过os.Getenv指定的可以不区分大小写，但建议大写。 运行输出：\n1 2 3 4 5 $ go run main.go name: awesome web version: 0.0.1 name: awesome web @buffer version: 0.0.1 @buffer 生成.env文件or字符串 可以通过程序生成一个.env文件的内容，可以直接写入到文件中：\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 package main import ( \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/joho/godotenv\u0026#34; ) func main() { buf := bytes.NewBuffer([]byte{}) buf.WriteString(\u0026#34;NAME=awesome web @write\u0026#34;) buf.WriteByte(\u0026#39;\\n\u0026#39;) buf.WriteString(\u0026#34;VERSION=0.0.1 @write\u0026#34;) envMap, err := godotenv.Parse(buf) if err != nil { log.Fatal(err) } err = godotenv.Write(envMap, \u0026#34;./write.env\u0026#34;) stringEnv, err := godotenv.Marshal(envMap) if err != nil { log.Fatal(err) } fmt.Println(stringEnv) } 通过Write方法，可以将map中的环境变量，写入到指定文件中。 通过Marshal方法，可以将map中的环境变量，序列化成字符串 运行会在当前目录下生成write.env文件：\n1 2 NAME=\u0026#34;awesome web @write\u0026#34; VERSION=\u0026#34;0.0.1 @write\u0026#34; TTY输出：\n1 2 3 $ go run main.go NAME=\u0026#34;awesome web @write\u0026#34; VERSION=\u0026#34;0.0.1 @write\u0026#34; 命令行模式 godotenv还提供了一个命令行的模式。要使用它，先要确保命令安装到$GOPATH/bin目录下：\n1 $ go install github.com/joho/godotenv/cmd/godotenv@latest 这个命令行程序，源码很简单，在github.com/joho/godotenv/cmd/godotenv路径下,用flag库解析的命令行参数。作用是读取env文件，写入环境变量中，不用在程序中调用godotenv。最后是通过Exec方法调用Load方法实现的。感兴趣的可以自己去看一下。\n安装后之后可以查看帮助信息，大致为：\n1 2 $ godotenv -h godotenv [-o] [-f ENV_FILE_PATHS] COMMAND_ARGS -o：是否覆盖环境变量，默认false -f：字段env文件，默认./.env 剩余参数：启动的程序 示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func main() { fmt.Println(os.Getenv(\u0026#34;NAME\u0026#34;)) fmt.Println(os.Getenv(\u0026#34;VERSION\u0026#34;)) fmt.Println(os.Getenv(\u0026#34;USERNAME\u0026#34;)) } ./.env文件：\n1 2 3 NAME: \u0026#39;awesome web\u0026#39; VERSION: 0.0.1 USERNAME: arlettebrook 使用godotenv命令启动程序,演示如下：\n1 2 3 4 5 6 7 8 9 $ godotenv go run main.go awesome web 0.0.1 Lenovo $ godotenv -o go run main.go awesome web 0.0.1 arlettebrook 第一次没有覆盖USERNAME,第二次覆盖了。\n通过godotenv命令行程序，我们可以不用再自己的程序中调用godotenv读取env文件。\n指定环境启动 实践中，一般会根据APP_ENV环境变量的值加载不同的文件：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/joho/godotenv\u0026#34; ) func main() { env := os.Getenv(\u0026#34;LEARN_ENV\u0026#34;) if env == \u0026#34;\u0026#34; { env = \u0026#34;development\u0026#34; } err := godotenv.Load(\u0026#34;.env.\u0026#34; + env) if err != nil { log.Fatal(err) } err = godotenv.Load() if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;name: \u0026#34;, os.Getenv(\u0026#34;NAME\u0026#34;)) fmt.Println(\u0026#34;version: \u0026#34;, os.Getenv(\u0026#34;VERSION\u0026#34;)) fmt.Println(\u0026#34;database: \u0026#34;, os.Getenv(\u0026#34;DATABASE\u0026#34;)) } 我们先读取环境变量LEARN_ENV，然后读取对应的.env. + env，最后读取默认的.env文件。\n前面也提到过，先读取到的优先。我们可以在默认的.env文件中配置基础信息和一些默认的值， 如果在开发/测试/生产环境需要修改，那么在对应的.env.development/.env.test/.env.production文件中再配置一次即可。\n.env文件内容：\n1 2 3 NAME: \u0026#39;awesome web\u0026#39; VERSION: 0.0.1 DATABASE: mongodb .env.development：\n1 DATABASE=sqlite .env.production：\n1 DATABASE=mysql 运行输出演示：\n1 2 3 4 5 6 7 8 9 10 11 # 默认是开发环境 $ go run main.go name: awesome web version: 0.0.1 database: sqlite # 用Load不会覆盖，所以表示mongodb # 设置为生成环境 $ LEARN_ENV=production go run main.go name: awesome web version: 0.0.1 database: mysql 一点源码 (其实你应该提前看一下源码~)\ngodotenv读取文件内容，为什么可以使用os.Getenv访问：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // github.com/joho/godotenv/godotenv.go func loadFile(filename string, overload bool) error { envMap, err := readFile(filename) if err != nil { return err } currentEnv := map[string]bool{} rawEnv := os.Environ() for _, rawEnvLine := range rawEnv { key := strings.Split(rawEnvLine, \u0026#34;=\u0026#34;)[0] currentEnv[key] = true } for key, value := range envMap { if !currentEnv[key] || overload { _ = os.Setenv(key, value) } } return nil } 因为godotenv调用os.Setenv将键值对设置到环境变量中了。就是在运行的时候修改了环境变量。\n总结 本文介绍了godotenv库的基础和高级用法。godotenv的源码也比较好读，有时间，有兴趣的童鞋建议一看~\n参考 godotenv GitHub 仓库： https://github.com/joho/godotenv 原文：Go 每日一库之 godotenv ","date":"2024-05-12T14:29:33+08:00","permalink":"https://arlettebrook.github.io/p/godotenv-introduction/","title":"Godotenv Introduction"},{"content":" 介绍 今天我们再来介绍Steve Francia（spf13）大神的另一个库cast。cast是一个小巧、实用的类型转换库，用于将一个类型转为另一个类型。它提供了一套高效且安全的类型转换功能。 最初开发cast是用在hugo中的。\n快速使用 先安装：\n1 $ go get -u github.com/spf13/cast 后使用：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/cast\u0026#34; ) func main() { // ToString fmt.Println(cast.ToString(\u0026#34;apple\u0026#34;)) // apple fmt.Println(cast.ToString(8)) // 8 fmt.Println(cast.ToString(8.31)) // 8.31 fmt.Println(cast.ToString([]byte(\u0026#34;one time\u0026#34;))) // one time fmt.Println(cast.ToString(nil)) // \u0026#34;\u0026#34; var foo interface{} = \u0026#34;one more time\u0026#34; fmt.Println(cast.ToString(foo)) // one more time // ToInt fmt.Println(cast.ToInt(8)) // 8 fmt.Println(cast.ToInt(8.31)) // 8 fmt.Println(cast.ToInt(\u0026#34;8\u0026#34;)) // 8 fmt.Println(cast.ToInt(true)) // 1 fmt.Println(cast.ToInt(false)) // 0 var eight interface{} = 8 fmt.Println(cast.ToInt(eight)) // 8 fmt.Println(cast.ToInt(nil)) // 0 } 实际上，cast实现了多种常见类型之间的相互转换，返回最符合直觉的结果。例如：\nnil转为string的结果为\u0026quot;\u0026quot;，而不是\u0026quot;nil\u0026quot;； true转为string的结果为\u0026quot;true\u0026quot;，而true转为int的结果为1； interface{}转为其他类型，要看它里面存储的值类型。 这些类型包括所有的基本类型（整形、浮点型、布尔值和字符串）、空接口、nil，时间（time.Time）、时长（time.Duration）以及它们的切片类型， 还有map[string]Type（其中Type为前面提到的类型）：\n1 2 3 4 byte bool float32 float64 string int8 int16 int32 int64 int uint8 uint16 uint32 uint64 uint interface{} time.Time time.Duration nil 基本使用 cast提供了两组函数：\nToType（其中Type可以为任何支持的类型），将参数转换为Type类型。如果无法转换，返回Type类型的零值或nil； ToTypeE以 E 结尾，返回转换后的值和一个error。这组函数可以区分参数中实际存储了零值，还是转换失败了。 源码分析： 实现上大部分代码都类似，ToType在内部调用ToTypeE函数，返回结果并忽略错误。ToType函数的实现在文件cast.go中， 而ToTypeE函数的实现在文件caste.go中。\n1 2 3 4 5 6 7 8 9 10 11 // cast/cast.go func ToBool(i interface{}) bool { v, _ := ToBoolE(i) return v } // ToDuration casts an interface to a time.Duration type. func ToDuration(i interface{}) time.Duration { v, _ := ToDurationE(i) return v } ToTypeE函数都接受任意类型的参数（interface{}），然后使用类型断言根据具体的类型来执行不同的转换。如果无法转换，返回错误。\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 45 46 47 48 // cast/caste.go // ToBoolE casts an interface to a bool type. func ToBoolE(i interface{}) (bool, error) { i = indirect(i) switch b := i.(type) { case bool: return b, nil case nil: return false, nil case int: return b != 0, nil case int64: return b != 0, nil case int32: return b != 0, nil case int16: return b != 0, nil case int8: return b != 0, nil case uint: return b != 0, nil case uint64: return b != 0, nil case uint32: return b != 0, nil case uint16: return b != 0, nil case uint8: return b != 0, nil case float64: return b != 0, nil case float32: return b != 0, nil case time.Duration: return b != 0, nil case string: return strconv.ParseBool(i.(string)) case json.Number: v, err := ToInt64E(b) if err == nil { return v != 0, nil } return false, fmt.Errorf(\u0026#34;unable to cast %#v of type %T to bool\u0026#34;, i, i) default: return false, fmt.Errorf(\u0026#34;unable to cast %#v of type %T to bool\u0026#34;, i, i) } } 首先调用indirect函数将参数中可能的指针去掉（返回原始类型）。如果类型本身不是指针，那么直接返回。否则返回指针指向的值。 循环直到返回一个非指针的值：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func indirect(a interface{}) interface{} { if a == nil { return nil } if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr { // Avoid creating a reflect.Value if it\u0026#39;s not a pointer. return a } v := reflect.ValueOf(a) for v.Kind() == reflect.Ptr \u0026amp;\u0026amp; !v.IsNil() { v = v.Elem() } return v.Interface() } 所以，下面代码输出都是 8：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/cast\u0026#34; ) func main() { /* 与make的区别： new创建指向该类型零值的指针 make创建指定的类型，并分配内存，用与引用类型 */ p := new(int) *p = 8 fmt.Println(cast.ToInt(p)) // 8 pp := \u0026amp;p fmt.Println(cast.ToInt(pp)) // 8 } 时间和时长转换 时间类型的转换代码如下：\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 // cast/caste.go func ToTimeInDefaultLocationE(i interface{}, location *time.Location) (tim time.Time, err error) { i = indirect(i) switch v := i.(type) { case time.Time: return v, nil case string: return StringToDateInDefaultLocation(v, location) case json.Number: s, err1 := ToInt64E(v) if err1 != nil { return time.Time{}, fmt.Errorf(\u0026#34;unable to cast %#v of type %T to Time\u0026#34;, i, i) } return time.Unix(s, 0), nil case int: return time.Unix(int64(v), 0), nil case int64: return time.Unix(v, 0), nil case int32: return time.Unix(int64(v), 0), nil case uint: return time.Unix(int64(v), 0), nil case uint64: return time.Unix(int64(v), 0), nil case uint32: return time.Unix(int64(v), 0), nil default: return time.Time{}, fmt.Errorf(\u0026#34;unable to cast %#v of type %T to Time\u0026#34;, i, i) } } 根据传入的类型执行不同的处理：\n如果是time.Time，直接返回； 如果是整型，将参数作为时间戳（自 UTC 时间1970.01.01 00:00:00到现在的秒数）调用time.Unix生成时间。Unix接受两个参数，第一个参数指定秒，第二个参数指定纳秒； 如果是字符串，调用StringToDateInDefaultLocation函数依次尝试以下面这些时间格式调用time.Parse解析该字符串。如果某个格式解析成功，则返回获得的time.Time。否则解析失败，返回错误； 其他任何类型都无法转换为time.Time。 字符串转换为时间：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 // cast/caste.go var ( timeFormats = []timeFormat{ // Keep common formats at the top. {\u0026#34;2006-01-02\u0026#34;, timeFormatNoTimezone}, {time.RFC3339, timeFormatNumericTimezone}, {\u0026#34;2006-01-02T15:04:05\u0026#34;, timeFormatNoTimezone}, // iso8601 without timezone {time.RFC1123Z, timeFormatNumericTimezone}, {time.RFC1123, timeFormatNamedTimezone}, {time.RFC822Z, timeFormatNumericTimezone}, {time.RFC822, timeFormatNamedTimezone}, {time.RFC850, timeFormatNamedTimezone}, {\u0026#34;2006-01-02 15:04:05.999999999 -0700 MST\u0026#34;, timeFormatNumericAndNamedTimezone}, // Time.String() {\u0026#34;2006-01-02T15:04:05-0700\u0026#34;, timeFormatNumericTimezone}, // RFC3339 without timezone hh:mm colon {\u0026#34;2006-01-02 15:04:05Z0700\u0026#34;, timeFormatNumericTimezone}, // RFC3339 without T or timezone hh:mm colon {\u0026#34;2006-01-02 15:04:05\u0026#34;, timeFormatNoTimezone}, {time.ANSIC, timeFormatNoTimezone}, {time.UnixDate, timeFormatNamedTimezone}, {time.RubyDate, timeFormatNumericTimezone}, {\u0026#34;2006-01-02 15:04:05Z07:00\u0026#34;, timeFormatNumericTimezone}, {\u0026#34;02 Jan 2006\u0026#34;, timeFormatNoTimezone}, {\u0026#34;2006-01-02 15:04:05 -07:00\u0026#34;, timeFormatNumericTimezone}, {\u0026#34;2006-01-02 15:04:05 -0700\u0026#34;, timeFormatNumericTimezone}, {time.Kitchen, timeFormatTimeOnly}, {time.Stamp, timeFormatTimeOnly}, {time.StampMilli, timeFormatTimeOnly}, {time.StampMicro, timeFormatTimeOnly}, {time.StampNano, timeFormatTimeOnly}, } ) func StringToDateInDefaultLocation(s string, location *time.Location) (time.Time, error) { return parseDateWith(s, location, timeFormats) } func parseDateWith(s string, location *time.Location, formats []timeFormat) (d time.Time, e error) { for _, format := range formats { if d, e = time.Parse(format.format, s); e == nil { // Some time formats have a zone name, but no offset, so it gets // put in that zone name (not the default one passed in to us), but // without that zone\u0026#39;s offset. So set the location manually. if format.typ \u0026lt;= timeFormatNamedTimezone { if location == nil { location = time.Local } year, month, day := d.Date() hour, min, sec := d.Clock() d = time.Date(year, month, day, hour, min, sec, d.Nanosecond(), location) } return } } return d, fmt.Errorf(\u0026#34;unable to parse date: %s\u0026#34;, s) } 时长类型的转换代码如下：\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 // cast/caste.go func ToDurationE(i interface{}) (d time.Duration, err error) { i = indirect(i) switch s := i.(type) { case time.Duration: return s, nil case int, int64, int32, int16, int8, uint, uint64, uint32, uint16, uint8: d = time.Duration(ToInt64(s)) return case float32, float64: d = time.Duration(ToFloat64(s)) return case string: if strings.ContainsAny(s, \u0026#34;nsuµmh\u0026#34;) { d, err = time.ParseDuration(s) } else { d, err = time.ParseDuration(s + \u0026#34;ns\u0026#34;) } return case json.Number: var v float64 v, err = s.Float64() d = time.Duration(v) return default: err = fmt.Errorf(\u0026#34;unable to cast %#v of type %T to Duration\u0026#34;, i, i) return } } 根据传入的类型进行不同的处理：\n如果是time.Duration类型，直接返回； 如果是整型或浮点型，将其数值强制转换为time.Duration类型，单位默认为ns； 如果是字符串，分为两种情况：如果字符串中有时间单位符号nsuµmh，直接调用time.ParseDuration解析；否则在字符串后拼接ns再调用time.ParseDuration解析； 其他类型解析失败。 时间、时长示例： 1 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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/spf13/cast\u0026#34; ) func main() { now := time.Now() timestamp := 1579615973 timeStr := \u0026#34;2020-01-21 22:13:48\u0026#34; fmt.Println(cast.ToTime(now)) // 2020-01-22 06:31:50.5068465 +0800 CST m=+0.000997701 fmt.Println(cast.ToTime(timestamp)) // 2020-01-21 22:12:53 +0800 CST fmt.Println(cast.ToTime(timeStr)) // 2020-01-21 22:13:48 +0000 UTC d, _ := time.ParseDuration(\u0026#34;1m30s\u0026#34;) ns := 30000 strWithS := \u0026#34;130s\u0026#34; strWithoutNs := \u0026#34;130\u0026#34; fmt.Println(cast.ToDuration(d)) // 1m30s fmt.Println(cast.ToDuration(ns)) // 30µs fmt.Println(cast.ToDuration(strWithS)) // 2m10s fmt.Println(cast.ToDuration(strWithoutNs)) // 130ns } 转换为切片 实际上，这些函数的实现基本类似。使用类型断言判断类型。如果就是要返回的类型，直接返回。否则根据类型进行相应的转换。\n我们主要分析两个实现：ToIntSliceE和ToStringSliceE。ToBoolSliceE/ToDurationSliceE与ToIntSliceE基本相同。\n首先是ToIntSliceE：\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 func ToIntSliceE(i interface{}) ([]int, error) { if i == nil { return []int{}, fmt.Errorf(\u0026#34;unable to cast %#v of type %T to []int\u0026#34;, i, i) } switch v := i.(type) { case []int: return v, nil } kind := reflect.TypeOf(i).Kind() switch kind { case reflect.Slice, reflect.Array: s := reflect.ValueOf(i) a := make([]int, s.Len()) for j := 0; j \u0026lt; s.Len(); j++ { val, err := ToIntE(s.Index(j).Interface()) if err != nil { return []int{}, fmt.Errorf(\u0026#34;unable to cast %#v of type %T to []int\u0026#34;, i, i) } a[j] = val } return a, nil default: return []int{}, fmt.Errorf(\u0026#34;unable to cast %#v of type %T to []int\u0026#34;, i, i) } } 根据传入参数的类型：\n如果是nil，直接返回错误； 如果是[]int，不用转换，直接返回； 如果传入类型为切片或数组，新建一个[]int，将切片或数组中的每个元素转为int放到该[]int中。最后返回这个[]int； 其他情况，不能转换。 ToStringSliceE：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 func ToStringSliceE(i interface{}) ([]string, error) { var a []string switch v := i.(type) { case []interface{}: for _, u := range v { a = append(a, ToString(u)) } return a, nil case []string: return v, nil case []int8: for _, u := range v { a = append(a, ToString(u)) } return a, nil case []int: for _, u := range v { a = append(a, ToString(u)) } return a, nil case []int32: for _, u := range v { a = append(a, ToString(u)) } return a, nil case []int64: for _, u := range v { a = append(a, ToString(u)) } return a, nil case []float32: for _, u := range v { a = append(a, ToString(u)) } return a, nil case []float64: for _, u := range v { a = append(a, ToString(u)) } return a, nil case string: return strings.Fields(v), nil case []error: for _, err := range i.([]error) { a = append(a, err.Error()) } return a, nil case interface{}: str, err := ToStringE(v) if err != nil { return a, fmt.Errorf(\u0026#34;unable to cast %#v of type %T to []string\u0026#34;, i, i) } return []string{str}, nil default: return a, fmt.Errorf(\u0026#34;unable to cast %#v of type %T to []string\u0026#34;, i, i) } } 根据传入的参数类型：\n如果是[]interface{}，将该参数中每个元素转为string，返回结果切片； 如果是[]string，不需要转换，直接返回； 如果是interface{}，将参数转为string，返回只包含这个值的切片； 如果是string，调用strings.Fields函数按空白符将参数拆分，返回拆分后的字符串切片； 其他情况，不能转换。 转换为切片示例： 1 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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/cast\u0026#34; ) func main() { sliceOfInt := []int{1, 3, 7} arrayOfInt := [3]int{8, 12} // ToIntSlice fmt.Println(cast.ToIntSlice(sliceOfInt)) // [1 3 7] fmt.Println(cast.ToIntSlice(arrayOfInt)) // [8 12 0] sliceOfInterface := []interface{}{1, 2.0, \u0026#34;apple\u0026#34;} sliceOfString := []string{\u0026#34;abc\u0026#34;, \u0026#34;dj\u0026#34;, \u0026#34;banana\u0026#34;} stringFields := \u0026#34; abc def hij hah\u0026#34; common := interface{}(37) // ToStringSliceE fmt.Println(cast.ToStringSlice(sliceOfInterface)) // [1 2 apple] fmt.Println(cast.ToStringSlice(sliceOfString)) toStringFields := cast.ToStringSlice(stringFields) fmt.Println(toStringFields, len(toStringFields)) // [abc dj banana hah] 4 // [abc def hij] fmt.Println(cast.ToStringSlice(common)) // [37] // ToToDurationSlice stringDurationSlice := []string{\u0026#34;1m23s\u0026#34;, \u0026#34;22h\u0026#34;} intDurationArray := [3]int{222222222, 88383838888} fmt.Println(cast.ToDurationSlice(stringDurationSlice)) // [1m23s 22h0m0s] fmt.Println(cast.ToDurationSlice(intDurationArray)) // [222.222222ms 1m28.383838888s 0s] // stringBoolSlice := []string{\u0026#34;true\u0026#34;, \u0026#34;false\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;T\u0026#34;} intBoolArray := [3]int{1, 0, 22} fmt.Println(cast.ToBoolSlice(stringBoolSlice)) // /*[true false true false true]*/ fmt.Println(cast.ToBoolSlice(intBoolArray)) // [true false true] } 转为map[string]Type类型 cast库能将传入的参数转为map[string]Type类型，Type为上面支持的类型。\n其实只需要分析一个ToStringMapStringE函数就可以了，其他的实现基本一样。ToStringMapStringE返回map[string]string类型的值。\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 func ToStringMapStringE(i interface{}) (map[string]string, error) { var m = map[string]string{} switch v := i.(type) { case map[string]string: return v, nil case map[string]interface{}: for k, val := range v { m[ToString(k)] = ToString(val) } return m, nil case map[interface{}]string: for k, val := range v { m[ToString(k)] = ToString(val) } return m, nil case map[interface{}]interface{}: for k, val := range v { m[ToString(k)] = ToString(val) } return m, nil case string: err := jsonStringToObject(v, \u0026amp;m) return m, err default: return m, fmt.Errorf(\u0026#34;unable to cast %#v of type %T to map[string]string\u0026#34;, i, i) } } 根据传入的参数类型：\n如果是map[string]string，不用转换，直接返回； 如果是map[string]interface{}，将每个值转为string存入新的 map，最后返回新的 map； 如果是map[interface{}]string，将每个键转为string存入新的 map，最后返回新的 map； 如果是map[interface{}]interface{}，将每个键和值都转为string存入新的 map，最后返回新的 map； 如果是string类型，cast将它看成一个 JSON 串，解析这个 JSON 到map[string]string，然后返回结果； 其他情况，返回错误。 转换为映射示例：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/cast\u0026#34; ) func main() { m1 := map[string]string{ \u0026#34;name\u0026#34;: \u0026#34;apple\u0026#34;, \u0026#34;job\u0026#34;: \u0026#34;developer\u0026#34;, } m2 := map[string]interface{}{ \u0026#34;name\u0026#34;: \u0026#34;banana\u0026#34;, \u0026#34;age\u0026#34;: 18, } m3 := map[interface{}]string{ \u0026#34;name\u0026#34;: \u0026#34;orange\u0026#34;, \u0026#34;job\u0026#34;: \u0026#34;designer\u0026#34;, } m4 := map[any]interface{}{ \u0026#34;name\u0026#34;: \u0026#34;strawberry\u0026#34;, \u0026#34;age\u0026#34;: 29, 18: \u0026#34;hi\u0026#34;, } jsonStr := `{\u0026#34;name\u0026#34;:\u0026#34;bibi\u0026#34;, \u0026#34;job\u0026#34;:\u0026#34;manager\u0026#34;}` fmt.Println(cast.ToStringMapString(m1)) // map[job:developer name:apple] fmt.Println(cast.ToStringMapString(m2)) // map[age:18 name:banana] fmt.Println(cast.ToStringMapString(m3)) // map[job:designer name:orange] fmt.Println(cast.ToStringMapString(m4)) // map[18:hi age:29 name:strawberry] fmt.Println(cast.ToStringMapString(jsonStr)) // map[job:manager name:bibi] } 总结 cast库能在几乎所有常见类型之间转换，使用非常方便。代码量也很小，有时间建议读读源码。常用于解析配置数据。\n参考 cast GitHub 仓库 原作者：Go 每日一库之 cast ","date":"2024-05-11T18:45:59+08:00","permalink":"https://arlettebrook.github.io/p/cast-introduction/","title":"Cast Introduction"},{"content":" Logrus 是目前 GitHub 上 Star 数量最多的 Go 日志库。尽管目前 Logrus 处于维护模式，不再引入新功能，但这并不意味着它已经死了。Logrus 仍将继续维护，以确保安全性、错误修复和提高性能。作为 Go 社区中最受欢迎的日志库之一，Logrus 最大的贡献是推动了 Go 社区广泛使用结构化（如JSON格式)的日志记录。著名的 Docker 项目就在使用 Logrus 记录日志，这进一步证明了其在实际应用中的可靠性和实用性。\n特点 Logrus 具有如下特点：\n与 Go log 标准库 API 完全兼容，这意味着任何使用 log 标准库的代码都可以将日志库无缝切换到 Logrus。 支持七种日志级别：Trace、Debug、Info、Warn、Error、Fatal、Panic。 支持结构化日志记录（key-value 形式，容易被程序解析，如 JSON 格式），通过 Filed 机制进行结构化的日志记录。 支持自定义日志格式，内置两种格式 JSONFormatter（JSON 格式） 和 TextFormatter（文本格式），并允许用户通过实现 Formatter 接口来自定义日志格式。 支持可扩展的 Hooks 机制，可以为不同级别的日志添加 Hooks 将日志记录到不同位置，例如将 Error、Fatal 和 Panic 级别的错误日志发送到 logstash、kafka 等。 支持在控制台输出带有不同颜色的日志。 并发安全。 快速使用 第三方库需要先安装：\n1 $ go get -u github.com/sirupsen/logrus 后使用：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package main import ( \u0026#34;github.com/sirupsen/logrus\u0026#34; ) func main() { logrus.SetLevel(logrus.TraceLevel) logrus.Trace(\u0026#34;trace msg\u0026#34;) logrus.Debug(\u0026#34;debug msg\u0026#34;) logrus.Info(\u0026#34;info msg\u0026#34;) logrus.Warn(\u0026#34;warn msg\u0026#34;) logrus.Error(\u0026#34;error msg\u0026#34;) logrus.Fatal(\u0026#34;fatal msg\u0026#34;) logrus.Panic(\u0026#34;panic msg\u0026#34;) } logrus的使用非常简单，与标准库log类似。logrus支持更多的日志级别：\nPanic：记录日志，然后panic。 Fatal：致命错误，出现错误时程序无法正常运转。输出日志后，程序退出； Error：错误日志，需要查看原因； Warn：警告信息，提醒程序员注意； Info：关键操作，核心流程的日志； Debug：一般程序中输出的调试信息； Trace：很细粒度的信息，一般用不到； 日志级别从上向下依次减小，Trace最小，Panic最大。logrus有一个日志级别，低于这个级别的日志不会输出。 默认的级别为InfoLevel。所以为了能看到Trace和Debug日志，我们在main函数第一行设置日志级别为TraceLevel。\n运行程序，非标准TTY输出：\n1 2 3 4 5 6 7 8 $ go run main.go time=\u0026#34;2024-05-09T11:31:42+08:00\u0026#34; level=trace msg=\u0026#34;trace msg\u0026#34; time=\u0026#34;2024-05-09T11:31:42+08:00\u0026#34; level=debug msg=\u0026#34;debug msg\u0026#34; time=\u0026#34;2024-05-09T11:31:42+08:00\u0026#34; level=info msg=\u0026#34;info msg\u0026#34; time=\u0026#34;2024-05-09T11:31:42+08:00\u0026#34; level=warning msg=\u0026#34;warn msg\u0026#34; time=\u0026#34;2024-05-09T11:31:42+08:00\u0026#34; level=error msg=\u0026#34;error msg\u0026#34; time=\u0026#34;2024-05-09T11:31:42+08:00\u0026#34; level=fatal msg=\u0026#34;fatal msg\u0026#34; exit status 1 logrus默认输出到标准错误。格式是文本格式，即默认的Formatter是TextFormatter。\n还有默认情况下，log.SetFormatter(\u0026amp;log.TextFormatter{})（即默认的TextFormatter）未连接 TTY 时，输出与 logfmt格式兼容（就是上面输出的格式）。当连接TTY时,会对输出的日志进行颜色编码，参考官方图片:\n为了确保即使连接了 TTY 也能实现不带颜色输出，请按如下方式设置格式化程序：\n1 2 3 logrus.SetFormatter(\u0026amp;log.TextFormatter{ DisableColors: true, }) 如果连接了TTY没有实现颜色输出（原因之一：非标准TTY、自定义的Formatter等)，需要颜色输出，请按如下方式设置格式化程序：\n1 2 3 4 logrus.SetFormatter(\u0026amp;logrus.TextFormatter{ ForceColors: true, // 强制输出颜色，原理：绕过TTY检查。 FullTimestamp: true, // 显示完整的时间戳 }) 当绕过TTY检查时会丢失日期时间，添加FullTimestamp: true,即可正常显示。\n后面会介绍更多格式化器。\n由于logrus.Fatal会导致程序退出，下面的logrus.Panic不会执行到。\n另外，我们观察到输出中有三个关键信息，time、level和msg：\ntime：输出日志的时间；为本地区标准时间。 补充：+08:00为北京标准时间，改为Z为UTC（世界标准时间，零时区时间） T为日期与时间的分隔符。是ISO定制的一种标准日期时间的表示方式。 level：日志级别； msg：日志信息。 使用 替代 Go log 标准库 在深入探究 Go log 标准库一文中举过一个使用 Go log 标准库的简单示例，现在可以将其无缝切换到 Logrus，只需要把 import \u0026quot;log\u0026quot; 改成 import log \u0026quot;github.com/sirupsen/logrus\u0026quot; 即可实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package main // 替代 import \u0026#34;log\u0026#34; import ( log \u0026#34;github.com/sirupsen/logrus\u0026#34; ) func main() { log.Print(\u0026#34;Print\u0026#34;) log.Printf(\u0026#34;Printf: %s\u0026#34;, \u0026#34;print\u0026#34;) log.Println(\u0026#34;Println\u0026#34;) log.Fatal(\u0026#34;Fatal\u0026#34;) log.Fatalf(\u0026#34;Fatalf: %s\u0026#34;, \u0026#34;fatal\u0026#34;) log.Fatalln(\u0026#34;Fatalln\u0026#34;) log.Panic(\u0026#34;Panic\u0026#34;) log.Panicf(\u0026#34;Panicf: %s\u0026#34;, \u0026#34;panic\u0026#34;) log.Panicln(\u0026#34;Panicln\u0026#34;) } 执行以上代码，得到如下输出：\n1 2 3 4 5 6 $ go run main.go time=\u0026#34;2024-05-09T14:13:43+08:00\u0026#34; level=info msg=Print time=\u0026#34;2024-05-09T14:13:43+08:00\u0026#34; level=info msg=\u0026#34;Printf: print\u0026#34; time=\u0026#34;2024-05-09T14:13:43+08:00\u0026#34; level=info msg=Println time=\u0026#34;2024-05-09T14:13:43+08:00\u0026#34; level=fatal msg=Fatal exit status 1 虽然输出格式与使用 Go log 标准库表现略有不同，但程序执行并不会报错，说明二者完全兼容。\n基本使用 修改日志级别 调用logrus.SetLevel(level Level)，就可以修改日志级别。\nlogrus默认的text记录器日志级别是InfoLevel。要想要输出info以下级别的日志，就必须修改。\n如\n1 logrus.SetLevel(logrus.TraceLevel) level可选择类型：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // logrus/logrus.go const ( // PanicLevel level, highest level of severity. Logs and then calls panic with the // message passed to Debug, Info, ... PanicLevel Level = iota // FatalLevel level. Logs and then calls `logger.Exit(1)`. It will exit even if the // logging level is set to Panic. FatalLevel // ErrorLevel level. Logs. Used for errors that should definitely be noted. // Commonly used for hooks to send errors to an error tracking service. ErrorLevel // WarnLevel level. Non-critical entries that deserve eyes. WarnLevel // InfoLevel level. General operational entries about what\u0026#39;s going on inside the // application. InfoLevel // DebugLevel level. Usually only enabled when debugging. Very verbose logging. DebugLevel // TraceLevel level. Designates finer-grained informational events than the Debug. TraceLevel ) 输出调用信息 调用logrus.SetReportCaller(true)，会在输出日志中添加方法、文件以及行号信息：\n1 2 3 4 5 6 7 8 9 10 11 package main import ( \u0026#34;github.com/sirupsen/logrus\u0026#34; ) func main() { logrus.SetReportCaller(true) logrus.Info(\u0026#34;info msg\u0026#34;) } 输出多了两个字段：func为函数名，file为调用logrus相关方法的文件名以及行号：\n1 2 3 $ go run main.go time=\u0026#34;2024-05-09T14:26:00+08:00\u0026#34; level=info msg=\u0026#34;info msg\u0026#34; func=main.main file=\u0026#34;F:/GoProject/learn /main.go:10\u0026#34; 添加字段 Logrus 鼓励用户通过日志字段记录结构化日志，可以使用 WithFields 和 WithField 两种形式，并且可以链式调用。\n尽量别用logrus.Fatalf(\u0026quot;Failed to send event %s to topic %s with key %d\u0026quot;) 这种纯文本形式，因为结构化日志有利于工具提取并分析日志。\n有时候需要在输出中添加一些字段，可以通过调用logrus.WithField（接收单个字段）和logrus.WithFields（接收多个字段）实现。 logrus.WithFields接受一个logrus.Fields类型的参数，其底层实际上为map[string]interface{}：\n1 2 // github.com/sirupsen/logrus/logrus.go type Fields map[string]interface{} 二者都可以链式调用，会返回一个指向Entry类型的结构体（日志条目logrus.Entry），该结构体绑定了各种级别的日志方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package main import ( \u0026#34;github.com/sirupsen/logrus\u0026#34; ) func main() { logrus. WithField(\u0026#34;name\u0026#34;, \u0026#34;arlettebrook\u0026#34;). WithField(\u0026#34;age\u0026#34;, 18). Info(\u0026#34;WithFiled\u0026#34;) logrus.WithFields(logrus.Fields{ \u0026#34;name\u0026#34;: \u0026#34;arlettebrook\u0026#34;, \u0026#34;age\u0026#34;: 18, }).Info(\u0026#34;WithFields\u0026#34;) } 运行输出：\n1 2 3 $ go run main.go time=\u0026#34;2024-05-09T15:22:24+08:00\u0026#34; level=info msg=WithFiled age=18 name=arlettebrook time=\u0026#34;2024-05-09T15:22:24+08:00\u0026#34; level=info msg=WithFields age=18 name=arlettebrook 默认字段：如果在一个函数中的所有日志都需要添加某些字段，可以使用WithFields的返回的*Entry替换logrus。这样后续输出都会包含指定字段：\n1 2 3 4 5 6 7 8 9 10 11 12 package main import \u0026#34;github.com/sirupsen/logrus\u0026#34; func main() { withFieldsLog := logrus.WithFields(logrus.Fields{ \u0026#34;name\u0026#34;: \u0026#34;arlettebrook\u0026#34;, \u0026#34;age\u0026#34;: 18, }) withFieldsLog.Error(\u0026#34;error msg\u0026#34;) withFieldsLog.Info(\u0026#34;info msg\u0026#34;) } 运行输出：\n1 2 3 $ go run main.go time=\u0026#34;2024-05-09T15:37:56+08:00\u0026#34; level=error msg=\u0026#34;error msg\u0026#34; age=18 name=arlettebrook time=\u0026#34;2024-05-09T15:37:56+08:00\u0026#34; level=info msg=\u0026#34;info msg\u0026#34; age=18 name=arlettebrook 使用同一个logrus.Entry调不同级别的日志方法，即可实现携带相同的字段（默认字段）。\n注意：默认字段也支持链式调用。\n重定向输出 默认情况下，日志输出到io.Stderr。可以调用logrus.SetOutput传入一个io.Writer参数。后续调用相关方法日志将写到io.Writer中。 现在，我们就能像介绍log时一样，可以搞点事情了。传入一个io.MultiWriter， 同时将日志写到bytes.Buffer、标准输出和文件中：\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 package main import ( \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/sirupsen/logrus\u0026#34; ) func main() { writer1 := bytes.NewBuffer(nil) writer2 := os.Stdout writer3, err := os.OpenFile(\u0026#34;./log.txt\u0026#34;, os.O_WRONLY|os.O_CREATE, 0755) defer func(writer3 *os.File) { err := writer3.Close() if err != nil { log.Fatal(err) } }(writer3) if err != nil { log.Fatalf(\u0026#34;create file log.txt failed: %v\u0026#34;, err) } logrus.SetOutput(io.MultiWriter(writer1, writer2, writer3)) logrus.Info(\u0026#34;info msg\u0026#34;) fmt.Println(\u0026#34;Buffer:\u0026#34;, writer1.String()) } 运行，会在文件log.txt和控制台输出日志。\n处理不同环境 Logrus 并没有像 zap 那样提供现成的 API 来支持在不同的环境下使用（Production 和 Development），如果你想在生产和测试环境使用不同的格式输出日志，则需要通过代码判断在不同环境设置不同的 Formatter 来实现。示例如下：\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 package main import ( \u0026#34;os\u0026#34; nested \u0026#34;github.com/antonfisher/nested-logrus-formatter\u0026#34; \u0026#34;github.com/sirupsen/logrus\u0026#34; ) func init() { // 假设环境变量APP_ENV已经被设置 env := os.Getenv(\u0026#34;APP_ENV\u0026#34;) // 根据环境设置日志级别 switch env { case \u0026#34;development\u0026#34;: // 在开发环境中显示所有日志 logrus.SetLevel(logrus.DebugLevel) logrus.SetFormatter(\u0026amp;nested.Formatter{}) case \u0026#34;testing\u0026#34;: // 在测试环境中只显示警告和错误日志 logrus.SetLevel(logrus.WarnLevel) case \u0026#34;production\u0026#34;: // 在生产环境中只显示错误日志 logrus.SetLevel(logrus.ErrorLevel) logrus.SetFormatter(\u0026amp;logrus.JSONFormatter{}) default: // 默认情况下，显示所有日志 logrus.SetLevel(logrus.DebugLevel) logrus.SetFormatter(\u0026amp;nested.Formatter{}) } // 你还可以设置日志格式、输出位置等 // ... } func main() { logrus.Error(\u0026#34;error log\u0026#34;) logrus.Warn(\u0026#34;warn log\u0026#34;) logrus.Info(\u0026#34;info log\u0026#34;) logrus.Debug(\u0026#34;debug log\u0026#34;) } 运行输出：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ go run main.go May 11 15:31:46.122 [ERRO] error log May 11 15:31:46.168 [WARN] warn log May 11 15:31:46.168 [INFO] info log May 11 15:31:46.169 [DEBU] debug log $ app_env=production go run main.go {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;error log\u0026#34;,\u0026#34;time\u0026#34;:\u0026#34;2024-05-11T15:31:53+08:00\u0026#34;} $ app_env=development go run main.go May 11 15:32:05.563 [ERRO] error log May 11 15:32:05.609 [WARN] warn log May 11 15:32:05.609 [INFO] info log May 11 15:32:05.609 [DEBU] debug log 测试 如果你的单元测试程序中需要测试日志内容，Logrus 提供了 test.NewNullLogger 日志记录器，它只会记录日志，不输出任何内容。使用示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package main import ( \u0026#34;testing\u0026#34; \u0026#34;github.com/sirupsen/logrus\u0026#34; \u0026#34;github.com/sirupsen/logrus/hooks/test\u0026#34; \u0026#34;github.com/stretchr/testify/assert\u0026#34; ) func TestLogrus(t *testing.T) { logger, hook := test.NewNullLogger() logger.Error(\u0026#34;Hello error\u0026#34;) assert.Equal(t, 1, len(hook.Entries)) assert.Equal(t, logrus.ErrorLevel, hook.LastEntry().Level) assert.Equal(t, \u0026#34;Hello error\u0026#34;, hook.LastEntry().Message) hook.Reset() assert.Nil(t, hook.LastEntry()) } 第二个返回值是一个结构体:\n1 2 3 4 5 6 7 type Hook struct { // Entries is an array of all entries that have been received by this hook. // For safe access, use the AllEntries() method, rather than reading this // value directly. Entries []logrus.Entry // 条目切片 mu sync.RWMutex } 方法LastEntry()返回Entries的最后一条日志条目对象。\n运行输出：\n1 2 3 4 5 $ go test -run TestLogrus -v === RUN TestLogrus --- PASS: TestLogrus (0.07s) PASS ok github.com/arlettebrook/learn 0.643s 自定义 Logger 除了通过 logrus.Info(\u0026quot;Info msg\u0026quot;) 这种开箱即用的方式使用 Logrus 默认的 Logger，我们还可以自定义 Logger。\n**实际上，考虑到易用性，库一般会使用默认值创建一个对象，包最外层的方法一般都是操作这个默认对象。**用到的设计模式是单例模式。\n我们之前好几篇文章都提到过这点：\nflag标准库中的CommandLine对象； log标准库中的std对象。 这个技巧应用在很多库的开发中，logrus也是如此：\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 // github.com/sirupsen/logrus/exported.go var ( // std is the name of the standard logger in stdlib `log` std = New() ) func New() *Logger { return \u0026amp;Logger{ Out: os.Stderr, Formatter: new(TextFormatter), Hooks: make(LevelHooks), Level: InfoLevel, ExitFunc: os.Exit, ReportCaller: false, } } func StandardLogger() *Logger { return std } // SetOutput sets the standard logger output. func SetOutput(out io.Writer) { std.SetOutput(out) } // SetFormatter sets the standard logger formatter. func SetFormatter(formatter Formatter) { std.SetFormatter(formatter) } // SetReportCaller sets whether the standard logger will include the calling // method as a field. func SetReportCaller(include bool) { std.SetReportCaller(include) } // SetLevel sets the standard logger level. func SetLevel(level Level) { std.SetLevel(level) } 首先，使用默认配置定义一个Logger对象std，SetOutput/SetFormatter/SetReportCaller/SetLevel这些方法都是调用std对象的对应方法！\n我们当然也可以创建自己的Logger对象，使用方式与直接调用logrus的方法类似：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package main import \u0026#34;github.com/sirupsen/logrus\u0026#34; func main() { log := logrus.New() log.Info(\u0026#34;info msg\u0026#34;) // 用自定义logger log.SetLevel(logrus.DebugLevel) log.SetFormatter(\u0026amp;logrus.JSONFormatter{}) log.Debug(\u0026#34;debug msg\u0026#34;) } New()函数创建的logger与默认的logger相同。运行输出：\n1 2 3 $ go run main.go time=\u0026#34;2024-05-09T22:55:30+08:00\u0026#34; level=info msg=\u0026#34;info msg\u0026#34; {\u0026#34;level\u0026#34;:\u0026#34;debug\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;debug msg\u0026#34;,\u0026#34;time\u0026#34;:\u0026#34;2024-05-09T22:55:30+08:00\u0026#34;} 通过创建的logger对象可以直接赋值修改Level、Out、Formatter等，不用调对应的Set方法。\n1 2 3 4 5 6 7 8 9 10 func New() *Logger { return \u0026amp;Logger{ Out: os.Stderr, Formatter: new(TextFormatter), Hooks: make(LevelHooks), Level: InfoLevel, ExitFunc: os.Exit, ReportCaller: false, } } 示例修改如下：\n1 2 3 log.Out=os.Stdout log.Level=logrus.DebugLevel log.Formatter=\u0026amp;logrus.JSONFormatter{} 我们还可以通过FieldMap属性修改默认字段的键名\n1 2 type FieldMap map[fieldKey]string type fieldKey string 支持重命名的默认字段fieldKey如下：\n1 2 3 4 5 6 7 8 9 const ( defaultTimestampFormat = time.RFC3339 FieldKeyMsg = \u0026#34;msg\u0026#34; FieldKeyLevel = \u0026#34;level\u0026#34; FieldKeyTime = \u0026#34;time\u0026#34; FieldKeyLogrusError = \u0026#34;logrus_error\u0026#34; FieldKeyFunc = \u0026#34;func\u0026#34; FieldKeyFile = \u0026#34;file\u0026#34; ) 实例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package main import ( \u0026#34;github.com/sirupsen/logrus\u0026#34; ) func main() { log := logrus.New() log.Formatter = \u0026amp;logrus.TextFormatter{ DisableColors: true, FieldMap: logrus.FieldMap{ logrus.FieldKeyMsg: \u0026#34;message\u0026#34;, // 将msg改成message }, } log.Info(\u0026#34;info msg\u0026#34;) log.Level = logrus.DebugLevel log.Formatter = \u0026amp;logrus.JSONFormatter{ // 会覆盖TextFormatter FieldMap: logrus.FieldMap{ logrus.FieldKeyTime: \u0026#34;TIME\u0026#34;,// 将 time改成TIME }, } log.Debug(\u0026#34;debug msg\u0026#34;) } 运行输出：\n1 2 3 $ go run main.go time=\u0026#34;2024-05-09T23:23:53+08:00\u0026#34; level=info message=\u0026#34;info msg\u0026#34; {\u0026#34;TIME\u0026#34;:\u0026#34;2024-05-09T23:23:53+08:00\u0026#34;,\u0026#34;level\u0026#34;:\u0026#34;debug\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;debug msg\u0026#34;} 修改日志格式 调用logrus.SetFormatter(formatter Formatter)，即可修改日志格式。\nlogrus支持两种日志格式，文本和 JSON，默认为文本格式。可以通过logrus.SetFormatter设置日志格式：\nLogrus 提供了 JSONFormatter 和 TextFormatter 来分别实现 JSON 和 Text 格式的日志输出，它们的指针类型都实现了 Formatter 接口。除此之外，这里还有一个第三方实现的 Formatter 列表可供选择，如果这些依然无法满足你的需求，则可以自己实现 Formatter 接口对象定制日志格式。\n使用如下：\n1 2 3 4 5 6 7 8 9 10 11 package main import \u0026#34;github.com/sirupsen/logrus\u0026#34; func main() { logrus.Info(\u0026#34;default formatter:TextFormatter\u0026#34;) //text logrus.SetFormatter(\u0026amp;logrus.JSONFormatter{}) logrus.Info(\u0026#34;JSONFormatter\u0026#34;) //json logrus.SetFormatter(\u0026amp;logrus.TextFormatter{}) logrus.Info(\u0026#34;default formatter:TextFormatter\u0026#34;) //text } logrus默认的Formatter是TextFormatter。在非标准TTY中运行输出结果（不带颜色输出）如下：\n1 2 3 4 $ go run main.go time=\u0026#34;2024-05-09T22:06:29+08:00\u0026#34; level=info msg=\u0026#34;default formatter:TextFormatter\u0026#34; {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;JSONFormatter\u0026#34;,\u0026#34;time\u0026#34;:\u0026#34;2024-05-09T22:06:29+08:00\u0026#34;} time=\u0026#34;2024-05-09T22:06:29+08:00\u0026#34; level=info msg=\u0026#34;default formatter:TextFormatter\u0026#34; 使用第三方格式 除了内置的TextFormatter和JSONFormatter，还有不少第三方格式支持。我们这里介绍一个nested-logrus-formatter。\n先安装：\n1 $ go get -u github.com/antonfisher/nested-logrus-formatter 后使用：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; nested \u0026#34;github.com/antonfisher/nested-logrus-formatter\u0026#34; \u0026#34;github.com/sirupsen/logrus\u0026#34; ) func main() { // 非标准TTY，强制输出颜色 logrus.SetFormatter(\u0026amp;logrus.TextFormatter{ ForceColors: true, FullTimestamp: true, TimestampFormat: time.DateTime, }) logrus.Info(\u0026#34;info msg\u0026#34;) logrus.WithFields(logrus.Fields{ \u0026#34;username\u0026#34;: \u0026#34;arlettebrook\u0026#34;, \u0026#34;age\u0026#34;: 18, }).Warn(\u0026#34;user info\u0026#34;) fmt.Println(\u0026#34;----------------\u0026#34;) logrus.SetFormatter(\u0026amp;nested.Formatter{ HideKeys: true, TimestampFormat: time.DateTime, }) logrus.Info(\u0026#34;info msg\u0026#34;) logrus.WithFields(logrus.Fields{ \u0026#34;username\u0026#34;: \u0026#34;arlettebrook\u0026#34;, \u0026#34;age\u0026#34;: 18, }).Warn(\u0026#34;user info\u0026#34;) } 程序运行输出：\n1 2 3 4 5 6 7 $ go run main.go INFO[2024-05-09 22:33:40] info msg WARN[2024-05-09 22:33:41] user info age=18 username=arlettebro ok ---------------- 2024-05-09 22:33:41 [INFO] info msg 2024-05-09 22:33:41 [WARN] [18] [arlettebrook] user info 没有截图，参考的是官方对比图片：\nnested格式提供了多个字段用来定制行为：\n1 2 3 4 5 6 7 8 9 10 // github.com/antonfisher/nested-logrus-formatter/formatter.go type Formatter struct { FieldsOrder []string TimestampFormat string HideKeys bool NoColors bool NoFieldsColors bool ShowFullLevel bool TrimMessages bool } 默认，logrus输出日志中字段是key=value这样的形式。使用nested格式，我们可以通过设置HideKeys为true隐藏键，只输出值；\n如果不隐藏键，程序输出：\n1 2024-05-09 22:40:09 [WARN] [age:18] [username:arlettebrook] user info 默认，logrus是按键的字母序输出字段，可以设置FieldsOrder定义输出字段顺序；string类型的切片指定顺序。\n通过设置TimestampFormat设置日期格式。如time.RFC3339、time.DateTime。\n通过实现接口logrus.Formatter可以实现自己的格式。\n1 2 3 4 // github.com/sirupsen/logrus/formatter.go type Formatter interface { Format(*Entry) ([]byte, error) } Hooks Hooks本质是一些函数或方法，用于不修改原代码，扩展程序。\nLogrus 最令人心动的两个功能，一个是结构化日志，另一个就是 Hooks 了。\nHooks 为 Logrus 提供了极大的灵活性，通过 Hooks 可以实现各种扩展功能。比如可以通过 Hooks 实现：Error 以上级别日志发送邮件通知、重要日志告警、日志切割、程序优雅退出等，非常实用。\nLogrus 提供了 Hook 接口，只要我们实现了这个接口，并将其注册到 Logrus 中，就可以使用 Hooks 的强大能力了。Hook 接口定义如下：\n1 2 3 4 type Hook interface { Levels() []Level Fire(*Entry) error } Levels 方法返回一个日志级别切片，Logrus 记录的日志级别如果存在于切片中，则会触发 Hooks，即调用 Fire 方法。\n为logrus设置钩子（Hooks），符合[]Level的日志输出前都会执行钩子的特定方法（Fire 方法）。所以，我们可以实现添加输出字段、根据级别将日志输出到不同的目的地。\n并且hook函数会在输出日志之前执行。\nEntry是当前日志条目（当前输出日志对象）。是结构体类型，定义如下：\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 type Entry struct { Logger *Logger // Contains all the fields set by the user. Data Fields // Time at which the log entry was created Time time.Time // Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic // This field will be set on entry firing and the value will be equal to the one in Logger struct field. Level Level // Calling method, with package name Caller *runtime.Frame // Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic Message string // When formatter is called in entry.log(), a Buffer may be set to entry Buffer *bytes.Buffer // Contains the context set by the user. Useful for hook processing etc. Context context.Context // err may contain a field formatting error err string } 常用的属性是：\nData Fields是日志条目中所有的字段，Fields类型是type Fields map[string]interface{}。 Logger *Logger记录该日志条目的logger。 单条日志条目信息都保存在Entry结构体中。如，创建时间、日志级别、日志消息等。 logrus也内置了一个syslog的钩子，将日志输出到系统日志syslog中。它不适用用windows的系统日志。\nLogrus 内置 Hooks 列表： https://github.com/sirupsen/logrus/tree/master/hooks\n自定义Hook 利用Hook添加字段 这里我们实现一个钩子，在输出的日志中增加一个app=awesome-web字段。\n示例代码如下：\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 package main import ( \u0026#34;github.com/sirupsen/logrus\u0026#34; ) type AppHook struct { AppName string } func (h *AppHook) Levels() []logrus.Level { return logrus.AllLevels } func (h *AppHook) Fire(entry *logrus.Entry) error { entry.Data[\u0026#34;app\u0026#34;] = h.AppName return nil } func main() { h := \u0026amp;AppHook{AppName: \u0026#34;awesome-web\u0026#34;} logrus.AddHook(h) logrus.Info(\u0026#34;info msg\u0026#34;) logrus.WithField(\u0026#34;username\u0026#34;, \u0026#34;arlettebrook\u0026#34;). Warn(\u0026#34;user info\u0026#34;) } 非标准TTY运行输出：\n1 2 3 4 $ go run main.go time=\u0026#34;2024-05-10T22:58:42+08:00\u0026#34; level=info msg=\u0026#34;info msg\u0026#34; app=awesome-web time=\u0026#34;2024-05-10T22:58:42+08:00\u0026#34; level=warning msg=\u0026#34;user info\u0026#34; app=awesome-web username=arlettebro ok 总结：添加钩子（hook），只需要创建一个结构体实现Hook接口，在Levels方法中设置触发hook函数的条件（日志级别），在Fire方法中定义hook函数行为。然后创建对象，利用AddHook(hook Hook)将hook注册到logger当中。\n利用Hook模拟邮件发送 代码如下：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/sirupsen/logrus\u0026#34; ) func init() { logrus.SetFormatter(\u0026amp;logrus.JSONFormatter{}) logrus.AddHook(\u0026amp;EmailHook{}) } type EmailHook struct { } func (e EmailHook) Levels() []logrus.Level { return logrus.AllLevels // 所有日志都发送到邮件 } func (e EmailHook) Fire(entry *logrus.Entry) error { // 添加一个邮箱字段标识 entry.Data[\u0026#34;app\u0026#34;] = \u0026#34;email\u0026#34; // 获取日志条目 msg, _ := entry.String() // 模拟发送邮件 fmt.Printf(\u0026#34;fakeSendEmail: %s\u0026#34;, msg) return nil } func main() { logrus.Info(\u0026#34;info msg\u0026#34;) logrus.WithField(\u0026#34;username\u0026#34;, \u0026#34;arlettebrook\u0026#34;). Warn(\u0026#34;user info\u0026#34;) } 运行输出：\n1 2 3 4 5 6 7 $ go run main.go fakeSendEmail: {\u0026#34;app\u0026#34;:\u0026#34;email\u0026#34;,\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;info msg\u0026#34;,\u0026#34;time\u0026#34;:\u0026#34;2024-05-10T23:22:13+08:00\u0026#34;} {\u0026#34;app\u0026#34;:\u0026#34;email\u0026#34;,\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;info msg\u0026#34;,\u0026#34;time\u0026#34;:\u0026#34;2024-05-10T23:22:13+08:00\u0026#34;} fakeSendEmail: {\u0026#34;app\u0026#34;:\u0026#34;email\u0026#34;,\u0026#34;level\u0026#34;:\u0026#34;warning\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;user info\u0026#34;,\u0026#34;time\u0026#34;:\u0026#34;2024-05-10T23:22:13+08:0 0\u0026#34;,\u0026#34;username\u0026#34;:\u0026#34;arlettebrook\u0026#34;} {\u0026#34;app\u0026#34;:\u0026#34;email\u0026#34;,\u0026#34;level\u0026#34;:\u0026#34;warning\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;user info\u0026#34;,\u0026#34;time\u0026#34;:\u0026#34;2024-05-10T23:22:13+08:00\u0026#34;,\u0026#34;username\u0026#34;:\u0026#34; arlettebrook\u0026#34;} 可以发现，在打印每条日志之前，都会执行Hook函数，也就是实现了发送邮件。\n第三方Hook logrus的第三方 Hook 有很多，我们可以使用一些现成的 Hook 。如：\nmgorus：将日志发送到 mongodb； logrus-redis-hook：将日志发送到 redis； logrus-amqp：将日志发送到 ActiveMQ。 lumberjackrus ：实现了日志切割和归档功能，并且能够将不同级别的日志输出到不同文件。lumberjackrus 是专门为 Logrus 而打造的文件日志 Hooks，其官方介绍为 local filesystem hook for Logrus。 更多过内容请参考官方提供的第三方开发的 Hooks 列表。\nlumberjackrus lumberjackrus ：实现了日志切割和归档功能，并且能够将不同级别的日志输出到不同文件。lumberjackrus 是专门为 Logrus 而打造的文件日志 Hooks，其官方介绍为 local filesystem hook for Logrus。\n用于记录到本地文件系统的钩子（使用 logrotate 和可以将不同的日志级保存到一个文件）\nlumberjackrus是一个第三方包，使用要先安装：\n1 $ go get -u github.com/orandin/lumberjackrus 示例如下：\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 45 46 47 48 49 50 package main import ( \u0026#34;github.com/orandin/lumberjackrus\u0026#34; \u0026#34;github.com/sirupsen/logrus\u0026#34; ) func init() { logrus.SetFormatter(\u0026amp;logrus.TextFormatter{}) logrus.SetLevel(logrus.DebugLevel) hook, err := lumberjackrus.NewHook( \u0026amp;lumberjackrus.LogFile{ // 未指定级别的日志保存的文件，属性都是optional Filename: \u0026#34;./tmp/general.log\u0026#34;, // 路径+文件名，默认当前目录名字\u0026lt;processName\u0026gt;-lumberjack.log MaxSize: 3, // 文件最大占用，单位MB，默认100MB MaxBackups: 1, // 文件最大备份数，默认不限制 MaxAge: 1, // 备份文件保存的天数，默认永久 Compress: false, // 是否压缩，默认false LocalTime: false, // 文件启用本地时间，默认utc， }, logrus.InfoLevel, // 定义写入文件的日志级别 \u0026amp;logrus.JSONFormatter{}, // 日志格式Formatter \u0026amp;lumberjackrus.LogFileOpts{ // 根据日志级别指定保存位置 logrus.InfoLevel: \u0026amp;lumberjackrus.LogFile{ Filename: \u0026#34;./tmp/info/info.log\u0026#34;, MaxSize: 1, MaxBackups: 2, MaxAge: 1, Compress: true, LocalTime: true, }, logrus.ErrorLevel: \u0026amp;lumberjackrus.LogFile{ Filename: \u0026#34;./tmp/error/error.log\u0026#34;, }, }, ) if err != nil { panic(err) } logrus.AddHook(hook) } func main() { logrus.Debug(\u0026#34;Debug message\u0026#34;) // It is not written to a file (because debug level \u0026lt; minLevel) logrus.Info(\u0026#34;Info message\u0026#34;) // Written in ./tmp/info.log logrus.Warn(\u0026#34;Warn message\u0026#34;) // Written in ./tmp/general.log logrus.Error(\u0026#34;Error message\u0026#34;) // Written in ./tmp/error.log } 运行，在非标准TTY中输出：\n1 2 3 4 5 $ go run main.go time=\u0026#34;2024-05-11T10:49:22+08:00\u0026#34; level=debug msg=\u0026#34;Debug message\u0026#34; time=\u0026#34;2024-05-11T10:49:22+08:00\u0026#34; level=info msg=\u0026#34;Info message\u0026#34; time=\u0026#34;2024-05-11T10:49:22+08:00\u0026#34; level=warning msg=\u0026#34;Warn message\u0026#34; time=\u0026#34;2024-05-11T10:49:22+08:00\u0026#34; level=error msg=\u0026#34;Error message\u0026#34; 并且还会再当前目录下生成tmp/general.log, tmp/info, tmp/error,里面的格式为json。debug日志没有写入文件。info.log文件内容如下：\n1 {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;Info message\u0026#34;,\u0026#34;time\u0026#34;:\u0026#34;2024-05-11T10:49:22+08:00\u0026#34;} 至此，我们使用lumberjackrus实现了日志轮询以及分级别保存。这个钩子，实现日志轮询是基于lumberjack库实现的。\n使用这个钩子，我们只需要利用NewHook(defaultLogger *LogFile, minLevel logrus.Level, formatter logrus.Formatter, opts *LogFileOpts) (*Hook, error)创建对象，将对象添加logger中即可使用。代码中有对参数的介绍。最后一个参数（本质是map类型）如果为nil或者为空map，那么日志将不会分级别保存，都会保存到defaultLogger指定的文件中。\nlogrus-redis-hook 下面演示利用hook将日志发送到redis，我们用到的包是logrus-redis-hook, 先安装logrus-redis-hook：\n1 $ go get -u github.com/rogierlommers/logrus-redis-hook 默认你已经安装好了redis并且启动了它，如果你想了解redis，可以参考我的另一篇文章《Redis Introduction》。\n然后编写程序：\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 45 46 package main import ( \u0026#34;io\u0026#34; logredis \u0026#34;github.com/rogierlommers/logrus-redis-hook\u0026#34; \u0026#34;github.com/sirupsen/logrus\u0026#34; ) func init() { logrus.SetFormatter(\u0026amp;logrus.JSONFormatter{}) hookConfig := logredis.HookConfig{ Host: \u0026#34;192.168.245.130\u0026#34;, Password: \u0026#34;123456\u0026#34;, Key: \u0026#34;demo_log\u0026#34;, Format: \u0026#34;v0\u0026#34;, App: \u0026#34;awesome_demo\u0026#34;, Port: 6379, Hostname: \u0026#34;localhost\u0026#34;, // will be sent to field @source_host DB: 1, // optional TTL: 3600, } hook, err := logredis.NewHook(hookConfig) if err == nil { logrus.AddHook(hook) } else { logrus.Errorf(\u0026#34;logredis error: %q\u0026#34;, err) } } func main() { // when hook is injected successfully, logs will be sent to redis server logrus.Info(\u0026#34;just some info logging...\u0026#34;) // we also support log.WithFields() logrus.WithFields(logrus.Fields{ \u0026#34;animal\u0026#34;: \u0026#34;walrus\u0026#34;, \u0026#34;foo\u0026#34;: \u0026#34;bar\u0026#34;, \u0026#34;this\u0026#34;: \u0026#34;that\u0026#34;}). Info(\u0026#34;additional fields are being logged as well\u0026#34;) // If you want to disable writing to stdout, use setOutput logrus.SetOutput(io.Discard) logrus.Info(\u0026#34;This will only be sent to Redis\u0026#34;) } 注意：请连接你自己的redis。\n运行程序后，终端输出：\n1 2 3 4 $ go run main.go {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;just some info logging...\u0026#34;,\u0026#34;time\u0026#34;:\u0026#34;2024-05-11T11:56:17+08:00\u0026#34;} {\u0026#34;animal\u0026#34;:\u0026#34;walrus\u0026#34;,\u0026#34;foo\u0026#34;:\u0026#34;bar\u0026#34;,\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;additional fields are being logged as well\u0026#34;,\u0026#34; this\u0026#34;:\u0026#34;that\u0026#34;,\u0026#34;time\u0026#34;:\u0026#34;2024-05-11T11:56:17+08:00\u0026#34;} 我们使用redis-cli查看：\n1 2 3 4 127.0.0.1:6379[1]\u0026gt; lrange demo_log 0 -1 1) \u0026#34;{\\\u0026#34;@fields\\\u0026#34;:{\\\u0026#34;application\\\u0026#34;:\\\u0026#34;awesome_demo\\\u0026#34;,\\\u0026#34;level\\\u0026#34;:\\\u0026#34;info\\\u0026#34;},\\\u0026#34;@message\\\u0026#34;:\\\u0026#34;just some info logging...\\\u0026#34;,\\\u0026#34;@source_host\\\u0026#34;:\\\u0026#34;localhost\\\u0026#34;,\\\u0026#34;@timestamp\\\u0026#34;:\\\u0026#34;2024-05-11T03:56:17.867867Z\\\u0026#34;}\u0026#34; 2) \u0026#34;{\\\u0026#34;@fields\\\u0026#34;:{\\\u0026#34;animal\\\u0026#34;:\\\u0026#34;walrus\\\u0026#34;,\\\u0026#34;application\\\u0026#34;:\\\u0026#34;awesome_demo\\\u0026#34;,\\\u0026#34;foo\\\u0026#34;:\\\u0026#34;bar\\\u0026#34;,\\\u0026#34;level\\\u0026#34;:\\\u0026#34;info\\\u0026#34;,\\\u0026#34;this\\\u0026#34;:\\\u0026#34;that\\\u0026#34;},\\\u0026#34;@message\\\u0026#34;:\\\u0026#34;additional fields are being logged as well\\\u0026#34;,\\\u0026#34;@source_host\\\u0026#34;:\\\u0026#34;localhost\\\u0026#34;,\\\u0026#34;@timestamp\\\u0026#34;:\\\u0026#34;2024-05-11T03:56:17.933777Z\\\u0026#34;}\u0026#34; 3) \u0026#34;{\\\u0026#34;@fields\\\u0026#34;:{\\\u0026#34;application\\\u0026#34;:\\\u0026#34;awesome_demo\\\u0026#34;,\\\u0026#34;level\\\u0026#34;:\\\u0026#34;info\\\u0026#34;},\\\u0026#34;@message\\\u0026#34;:\\\u0026#34;This will only be sent to Redis\\\u0026#34;,\\\u0026#34;@source_host\\\u0026#34;:\\\u0026#34;localhost\\\u0026#34;,\\\u0026#34;@timestamp\\\u0026#34;:\\\u0026#34;2024-05-11T03:56:17.9351074Z\\\u0026#34;}\u0026#34; 我们看到demo_log是一个list，每过来一条日志，就在list后新增一项。\n默认的logger是输出到stderr，但修改为discard，将不会输出到任何地方，但是钩子依然执行。因为钩子里面指定发送到的是redis，不受影响。\n总结 本文介绍了 Logrus 的基本特点，以及如何使用。\nLogrus 完全兼容 log 标准库，所以可以实现无缝替换。其 API 设计思路跟 log 标准库的风格也有很多相似之处，都提供了一个默认的 std 日志对象达到开箱即用的效果。Logrus 最实用的两个功能，一个是支持结构化日志，一个是支持 Hooks 机制，这极大的提升了可用性和灵活性，也使得 Logrus 成为最受欢迎的 Go 日志库。\n参考 logrus GitHub 仓库 Go 每日一库之 logrus Go 第三方 log 库之 logrus 使用 ","date":"2024-05-09T10:31:23+08:00","permalink":"https://arlettebrook.github.io/p/logrus-introduction/","title":"Logrus Introduction"},{"content":" Linux常用命令有很多，本文不会逐个介绍。以下命令是我在后期使用中遇到的，算是我对Linux常用命令的补充、回顾、总结。本篇文章阅读需要一定的Linux基础。更多常见命令可参考：600条Linux命令总结。\n持续更新中\u0026hellip;\nln ln命令在Linux系统中用于创建文件链接。\nLinux中文件链接的方式有两种： 符号链接（也称为软链接Symbolic Link）：包含了到原文件的路径信息，相当于一个指向原文件的快捷方式。 符号链接有自己的文件属性及权限等。 可对不存在的文件或目录创建符号链接。 符号链接可交叉文件系统，即可以在不同的文件系统之间创建。 删除符号链接并不影响被指向的文件，但若被指向的原文件被删除，则相关符号链接被称为死链接（dangling link）。若被指向的文件重新被创建，死链接可恢复为正常的符号链接。 符号链接文件的大小是其指向的文件的路径字符串的字节数。 硬链接：硬链接本质上是给一个文件取一个新的名称，原文件和硬链接在物理上仍然是同一个文件。它们共享相同的inode（索引节点）和数据块。 创建硬链接会在对应的目录中增加额外的记录项以引用文件。 原文件和硬链接文件对应于同一文件系统上的一个物理文件。 创建硬链接时原文件的连接数（i_nlink）会递增。即硬链接数递增。 删除文件时，rm命令会递减计数的链接数。文件要是存在，至少有一个链接数。当链接数为零时，该文件才会被真正删除。 硬链接不能跨域驱动器或分区，也不支持对目录创建硬链接。 总结来说，符号链接（软链接）和硬链接的主要区别在于： 符号链接是一个独立的文件，它包含了到原文件的路径信息（快捷方式）；而硬链接则与原文件共享相同的inode和数据块（文件别名）。 符号链接可以跨文件系统，并且可以对不存在的文件或目录创建；而硬链接则不能跨域驱动器或分区，也不能对目录创建。 删除符号链接不影响原文件，但删除原文件会使符号链接成为死链接；而删除硬链接文件时，只有当所有硬链接都被删除且原文件的链接数为零时，文件才会被真正删除。即硬链接数为0，文件才真正被删除。 这个命令允许你在不同的位置为同一个文件或目录建立同步的链接，从而避免了在多个位置重复存储相同的文件内容，节省了磁盘空间。\nln命令的基本语法如下：\n1 ln [选项] \u0026lt;源文件或目录\u0026gt; \u0026lt;链接文件或目录\u0026gt; 常用选项包括： -s：创建符号链接（软链接）。这会在你选定的位置上生成一个文件的镜像，不会占用磁盘空间，而是保存了原始文件的路径。（创建快捷方式） 不使用-s选项时创建硬链接。 -f：强制执行，如果链接文件已存在则覆盖。 -v：显示详细的输出信息。 --help：查看帮助文档。 理解记忆：ln可以理解为link的缩写，s可以理解为soft软的意思。 注意：源文件一般用绝对路径，相对会出现死链接情况。 简单介绍一些ls -lh输出\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ ls -lh total 20K -rw-r--r--. 1 root root 19K May 8 22:49 abc.txt drwxr-xr-x. 2 root root 18 May 8 23:15 demo # l显示详细信息，h文件大小用人类可读懂的方式显示。 -rw- r-- r-- -开头表示文件，d表示目录，l表示链接，三位为一组用rwx drwx r-x r-x 当前用户权限 用户所属组权限\t其他用户权限 数字1表示硬链接数，理解为别名，别名为0，文件才会被删除。 有多少种方式可以访问该文件或目录 文件一般为1：绝对路径 目录只有一层一般为2(不包含子目录）：绝对和cd .. root ：拥有者 root ：所在组，没指定默认同名。 5月 8 22:49 文件最后修改日期时间 最后：文件或目录名称 有箭头表示软连接，没有硬链接。 演示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 localhost test $ ls -lh total 20K -rw-r--r--. 1 root root 19K May 8 22:49 abc.txt drwxr-xr-x. 2 root root 18 May 8 23:46 demo localhost test $ ln -sv ~/test/abc.txt ./demo/soft ‘./demo/soft’ -\u0026gt; ‘/root/test/abc.txt’ localhost test $ ln -v ~/test/abc.txt ./demo/hard ‘./demo/hard’ =\u0026gt; ‘/root/test/abc.txt’ localhost test $ ls -lh total 20K -rw-r--r--. 2 root root 19K May 8 22:49 abc.txt drwxr-xr-x. 3 root root 42 May 8 23:46 demo localhost test $ cd demo localhost demo $ ls -lh total 20K -rw-r--r--. 2 root root 19K May 8 22:49 hard lrwxrwxrwx. 1 root root 18 May 8 23:46 soft -\u0026gt; /root/test/abc.txt 注意：\n测试环境是在Linux中，ln命令并不适用于windows环境。创建的文件链接无效。 目录只能创建软链接，不能创建硬链接。 文件两种链接都可以创建。 curl curl命令是一个利用URL规则在命令行下工作的文件传输工具，它支持文件的上传和下载，因此被视为综合传输工具，但传统上习惯称其为下载工具。curl支持包括HTTP、HTTPS、FTP等众多协议，并具备多种特性，如POST、cookies、认证、从指定偏移处下载部分文件、用户代理字符串、限速、文件大小、进度条等。这些特性使得curl在处理网页处理流程和数据检索自动化方面非常有用。\n总结：用于发送网络请求，可以下载和上传文件。而wget只能用于下载文件，但wget下载文件的功能比curl强大。\n安装：\nlinux系统：根据你的Linux发行版，使用相应的包管理器来安装curl。 对于Debian/Ubuntu系统，使用apt-get命令安装：sudo apt install curl 对于CentOS/Fedora系统，使用yum命令安装：sudo yum install curl 对于Arch Linux系统，使用pacman命令安装：sudo pacman -S curl windows系统： 官网下载对应版本，添加一个CURL_HOME环境变量名，指向的是curl.exe文件所在路径。并将其添加到path环境变量中。 或者安装msys2利用pacman包管理器安装。【推荐】：sudo pacman -S curl 或者安装Mingw自带curl，Mingw也可以用msys2来安装。 MacOS系统自带。 基本使用：\ncurl命令的基本语法格式为：curl [选项] URL。其中，URL是要请求的目标地址，而选项则用于指定各种参数和功能。\n-X 或 --request：用于指定HTTP请求方法，如GET、POST、PUT、DELETE等。\n默认发送get请求。 1 curl -X POST http://example.com/ -H 或 --header: 添加自定义的 HTTP 头信息。\n1 curl -H \u0026#34;Content-Type: application/json\u0026#34; http://example.com/ -d 或 --data: 发送 POST 请求时，将数据作为请求体发送。\n1 curl -d \u0026#34;param1=value1\u0026amp;param2=value2\u0026#34; http://example.com/ 对于 JSON 数据：\n1 curl -d \u0026#39;{\u0026#34;key1\u0026#34;:\u0026#34;value1\u0026#34;, \u0026#34;key2\u0026#34;:\u0026#34;value2\u0026#34;}\u0026#39; -H \u0026#34;Content-Type: application/json\u0026#34; http://example.com/ -F 或 --form: 用于发送 multipart/form-data 格式的数据，通常用于文件上传。\n参数\u0026quot;文件名=@path\u0026quot;,还可以指定类型\u0026quot;文件名=@path;type=MIME\u0026quot; 1 curl -F \u0026#34;file=@filename.txt\u0026#34; http://example.com/upload -o 或 --output: 将响应输出到文件，而不是显示在终端上。默认当前目录\n1 curl -o output.html http://example.com/ -O: 将响应保存到本地文件，文件名与远程文件的文件名相同。在当前目录\n1 curl -O http://example.com/file.txt -s 或 --silent: 静默模式，不显示进度或错误消息。常用S组合使用，显示错误消息。\n-v 或 --verbose: 详细模式，显示详细的通信过程，包括请求头和响应头。\n-i: 显示响应头信息以及返回的内容 body。\n-I: 只显示响应头信息。\n-u 用户名:密码: 指定用户名和密码进行身份验证。\n1 curl -u username:password http://example.com/ -L: 跟随重定向。当 HTTP 响应是一个重定向时，curl 会自动获取重定向后的内容。\n-k 或 --insecure: 允许连接到不安全的 SSL 站点，即忽略 SSL 证书验证。\n常用组合：curl -fsSL，默认get请求。\nf不显示客户端错误。 s不显示进度、错误消息。配合S显示错误消息 L接受重定向。 wget wget 是一个在 Unix 和 Linux 系统上常用的命令行工具，用于从网络上下载文件。\n优点：\n支持断点续传：如果下载过程中连接中断，可以在之后从断点处继续下载。 支持递归下载：可以下载网页上的所有链接文件，并重建目录结构。 强大的重试机制：在下载过程中如果网络出现问题，wget 会自动重试，直到下载完成。 后台执行：支持在后台运行下载任务，用户无需保持登录状态。 curl没有以上优点，但它支持更多的协议。wget只支持HTTP、HTTPS和FTP协议。curl使用更广泛。\n安装：\nlinux系统：根据你的Linux发行版，使用相应的包管理器来安装wget。\n对于Debian/Ubuntu系统，使用apt-get命令安装：sudo apt install wget\n对于CentOS/Fedora系统，使用yum命令安装：sudo yum install wget\n对于Arch Linux系统，使用pacman命令安装：sudo pacman -S wget\nwindows系统：\n官网GNU Wget下载安装或其他可靠的软件下载，然后路径添加到环境变量path中。 或者安装msys2利用pacman包管理器安装。【推荐】：sudo pacman -S wget macos系统：\n对于macOS，使用Homebrew包管理器来安装wget。\n1 brew install wget 基本使用：\n-b, \u0026ndash;background：在后台执行下载任务。\n-c, \u0026ndash;continue：继续下载之前未完成的文件。\n-r, \u0026ndash;recursive：递归下载，下载指定 URL 中的所有链接。可以下载整个目录及其子目录。\n1 wget -r http://www.example.com/ 注意：在使用递归下载时，建议加上 -np 和 -nH 选项，以避免下载过多的不必要文件和创建复杂的目录结构。\n-np, \u0026ndash;no-parent：不递归下载上级目录。\n-nH, \u0026ndash;no-host-directories：不创建主机目录，将文件保存在当前目录。\n-P DIRECTORY, \u0026ndash;directory-prefix=DIRECTORY：将下载的文件保存到指定的目录。\n1 wget -P /tmp http://www.example.com/file.zip -O FILE, \u0026ndash;output-document=FILE：将下载的文件保存为指定的文件名。如果存在会覆盖。-N（时间戳检查），如果旧会覆盖，反之不会。\n1 wget -O new_file.zip http://www.example.com/file.zip -nc, \u0026ndash;no-clobber：如果文件已经存在，不覆盖原有文件。\n-nv, \u0026ndash;no-verbose：下载时只显示更新和出错信息，不显示详细的执行过程。\n-v, \u0026ndash;verbose：详细模式，增加输出信息。\n-q, \u0026ndash;quiet：静默模式，减少输出信息。\n\u0026ndash;no-check-certificate：下载 HTTPS 网站资源时，跳过证书检测过程。\n-t NUM 或 \u0026ndash;tries=NUM：指定最大尝试次数。如果下载失败，wget 会尝试重新下载指定的次数。\n-U AGENT 或 \u0026ndash;user-agent=AGENT：指定 User-Agent 字符串。这可以用来伪装为不同的浏览器或客户端。\n-N 或 \u0026ndash;timestamping：如果本地文件存在且时间戳较新，则不重新下载文件。\n-T SEC 或 \u0026ndash;timeout=SEC：指定超时时间（以秒为单位）。\n-A TYPES 或 \u0026ndash;accept=TYPES：指定下载文件的类型。例如，-A jpg,png,gif 表示只下载 jpg、png 和 gif 文件。\n-d 或 \u0026ndash;debug：打印调试输出。这会在标准错误上输出大量信息，通常用于诊断问题。\n常用组合：\nwget -O-:O表示指定文件名，后面-表示没有指定文件名，内容将输出到TTY。\n通常配合bash -c \u0026quot;$(wget -O- url)\u0026quot;运行脚本。url为脚本地址。\n其中$()表示在当前TTY中运行子命令，下载脚本。\n[bash -c](#bash -c)表示将下载的字符串做为命令执行。\nbash -c bash -c 命令用于在 Bash shell 中执行一个字符串作为命令。\n基本语法：\n1 bash -c \u0026#39;command_string\u0026#39; [arg0 [arg1 ...]] command_string 是你想要执行的命令字符串。 arg0、arg1 等是可选的，它们会被用作 $0、$1、$2 等 shell 变量在 command_string 内部。 注意这里的$0并不是脚本名或终端名，而是第一个参数。 字符串可以用\u0026quot;\u0026quot;/''，区别在于双引号会转义特殊字符，单引号不会。执行脚本时建议双引号。 示例：\n1 2 3 4 5 6 7 $ bash -c \u0026#39;echo Hello, World!\u0026#39; Hello, World! $ bash -c \u0026#39;echo $0 $1\u0026#39; bash test bash test # bash -c \u0026#34;$(wget -O- https://gist.githubusercontent.com/lss233/2fdd75be3f0724739368d0dcd9d1367d/raw/62a790da4a391af096074b3355c2c2b7ecab3c28/chatgpt-mirai-installer-gocqhttp.sh)\u0026#34; chatgpt-qq-bot安装脚本 pacman Pacman是Arch Linux及其衍生发行版（如Manjaro、EndeavourOS等）使用的包管理器。总结它，是因为我们可以在Windows上安装msys2来使用pacman包管理器，进而在Windows上安装Linux命令。\n基本用法：\n1 sudo pacman [选项] [软件名] 常用选项如下：\n-S：安装软件包、软件源列表。 -y：刷新软件源列表。 -u：更新软件包到最新版。 以上三个选项可以组合使用。常用： -Syu：更新软件源列表，并升级已安装的包到最新版。【推荐】* -Sy：更新软件源列表并安装。【推荐】* -Su：只升级已安装的包到最新版。 -S package_name：安装软件包。多个用空格隔开。【推荐】* -R package_name：卸载软件包（会保留依赖）。 -Rs package_name：卸载一个软件包及其依赖（未被其他包使用的依赖）。 -Rns package_name：卸载一个软件包并删除不再使用的依赖。【推荐】* -Sc：清理未使用的包缓存。 -Scc：清理所有包缓存。 -Ss keyword：搜索包含关键字的软件包。 -Si package_name：显示软件包信息。【推荐】* -Q：列出所有已安装的软件包。 -Qs keyword：列出已安装的包含关键字的软件包。【推荐】 -Qi package_name：显示已安装软件包的信息。【推荐】* -Ql package_name：列出软件包中包含的文件。【推荐】 -Qk：检查系统中所有已安装包的完整性。 -Qkk package_name：检查指定软件包的完整性。 ","date":"2024-05-08T19:24:26+08:00","permalink":"https://arlettebrook.github.io/p/linux-common-commands/","title":"Linux Common Commands"},{"content":" Viper 可以监听文件修改进而自动重新加载。 其内部使用的就是fsnotify这个库，它是跨平台的。fs是filesystem的缩写，翻译过来就是文件系统通知。能够监听文件的修改，进而发送通知。今天我们就来介绍一下fsnotify。\n快速使用 先安装：\n1 $ go get -u github.com/fsnotify/fsnotify 后使用：\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 45 46 47 48 49 50 51 52 53 package main import ( \u0026#34;log\u0026#34; \u0026#34;github.com/fsnotify/fsnotify\u0026#34; ) func main() { // Create new watcher. watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer func(watcher *fsnotify.Watcher) { err := watcher.Close() if err != nil { log.Fatal(err) } }(watcher) done := make(chan bool) // Start listening for events. go func() { defer close(done) for { select { case event, ok := \u0026lt;-watcher.Events: if !ok { return } log.Printf(\u0026#34;%s %s\\n\u0026#34;, event.Name, event.Op) /*if event.Has(fsnotify.Write) { log.Println(\u0026#34;modified file:\u0026#34;, event.Name) }*/ case err, ok := \u0026lt;-watcher.Errors: if !ok { return } log.Println(\u0026#34;error:\u0026#34;, err) } } }() // Add a path. err = watcher.Add(\u0026#34;.\u0026#34;) if err != nil { log.Fatal(err) } \u0026lt;-done } fsnotify的使用比较简单：\n先调用NewWatcher创建一个监听器； 然后调用监听器的Add增加监听的文件或目录； 如果目录或文件有事件产生，监听器中的通道Events可以取出事件。如果出现错误，监听器中的通道Errors可以取出错误信息。 上面示例中，我们在另一个 goroutine 中循环读取发生的事件及错误，然后输出它们。\n编译、运行程序。在当前目录创建一个新建文本文档.txt，然后重命名为abc.txt文件，输入内容some test text，然后删除它。观察控制台输出：\n1 2 3 4 5 6 $ go run main.go 2024/05/08 16:29:52 新建文本文档.txt CREATE 2024/05/08 16:30:03 新建文本文档.txt RENAME 2024/05/08 16:30:03 abc.txt CREATE 2024/05/08 16:30:15 abc.txt WRITE 2024/05/08 16:30:26 abc.txt REMOVE 其实，重命名时会产生两个事件，一个是原文件的RENAME事件，一个是新文件的CREATE事件。\n注意：\nfsnotify使用了操作系统接口，监听器中保存了系统资源的句柄，所以使用后需要关闭。 修改文件操作建议不要在IDE中操作，如GlLand。IDE的缓存和自动保存会响应输出结果。建议直接在系统文件管理器中操作，才会出现上面结果。 事件 上面示例中的事件是fsnotify.Event类型：\n1 2 3 4 5 // fsnotify/fsnotify.go type Event struct { Name string Op Op } 事件只有两个字段，Name表示发生变化的文件或目录名，Op表示具体的变化。Op有 5 种取值：\n1 2 3 4 5 6 7 8 9 10 // fsnotify/fsnotify.go type Op uint32 const ( Create Op = 1 \u0026lt;\u0026lt; iota Write Remove Rename Chmod ) 在快速使用中，我们已经演示了前 4 种事件。Chmod事件在文件或目录的属性发生变化时触发，在 Linux 系统中可以通过chmod命令改变文件或目录属性。\n事件中的Op是按照左移位运算来存储的，可以存储多个，可以通过\u0026amp;操作判断对应事件是不是发生了。\n1 2 3 if event.Op \u0026amp; fsnotify.Write != 0 { fmt.Println(\u0026#34;Op has Write\u0026#34;) } 补充：\n​\t与运算\u0026amp;：同为1为1。\n​\t左移运算\u0026laquo;：向做做移动指定位，低位用0补齐\n​\t或运算：有1就为1。用在组合Op——\u0026gt;|\n​\t事件中的Op是通过左移运算之后，结果为1,2,4,6,8十进制来存储的，它们的二进制位都只有一个1，当进行与运算时，就就能判断是否包含指定Op。\n判断事件中是否存在某个Op，封装到了事件对象的Has方法下：\n1 2 3 4 // fsnotify/fsnotify.go func (e Event) Has(op Op) bool { return e.Op.Has(op) } func (o Op) Has(h Op) bool { return o\u0026amp;h != 0 } // event.Op \u0026amp; fsnotify.Write != 0同理 当我们直接输出Op时，会自动调用String()方法，这个方法帮我们将对应的Op（uint类型）转换成定义的具体操作，代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func (o Op) String() string { var b strings.Builder if o.Has(Create) { b.WriteString(\u0026#34;|CREATE\u0026#34;) } if o.Has(Remove) { b.WriteString(\u0026#34;|REMOVE\u0026#34;) } if o.Has(Write) { b.WriteString(\u0026#34;|WRITE\u0026#34;) } if o.Has(Rename) { b.WriteString(\u0026#34;|RENAME\u0026#34;) } if o.Has(Chmod) { b.WriteString(\u0026#34;|CHMOD\u0026#34;) } if b.Len() == 0 { return \u0026#34;[no events]\u0026#34; } return b.String()[1:] } 应用 fsnotify的应用非常广泛，在 godoc 上，我们可以看到哪些库导入了fsnotify。只需进入fsnotify godoc点击Imported by 9,171，就能查看。有兴趣的可以打开看看。\n在《Go配置管理之第三方库viper》文章中，我们介绍了调用viper.WatchConfig就可以监听配置修改，自动重新加载。下面我们就来看看WatchConfig是怎么实现的：\n首先演示viper自动更新配置\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 package main // mian.go import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/fsnotify/fsnotify\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetConfigFile(\u0026#34;./cfg.yaml\u0026#34;) err := viper.ReadInConfig() if err != nil { log.Fatal(\u0026#34;ReadInConfig error:\u0026#34;, err) } viper.OnConfigChange(func(in fsnotify.Event) { log.Printf(\u0026#34;%s %s username：%s\\n\u0026#34;, in.Name, in.Op, viper.Get(\u0026#34;username\u0026#34;)) }) fmt.Println(\u0026#34;修改前的username：\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) viper.WatchConfig() time.Sleep(10 * time.Second) // 修改文件 fmt.Println(\u0026#34;修改后的username：\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) } 1 2 # ./cfg.yaml username: arlettebrook 运行之后，手动修改cfg.yamlusername（注意不要在IDE中修改，会影响输出效果）,在后面分别追加1/2/3,并别保存，输出：\n1 2 3 4 5 6 $ go run main.go 修改前的username： arlettebrook 2024/05/08 18:01:32 cfg.yaml WRITE username：arlettebrook1 2024/05/08 18:01:36 cfg.yaml WRITE username：arlettebrook12 2024/05/08 18:01:39 cfg.yaml WRITE username：arlettebrook123 修改后的username： arlettebrook123 具体实现：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 // viper/viper.go // WatchConfig starts watching a config file for changes. func WatchConfig() { v.WatchConfig() } // WatchConfig starts watching a config file for changes. func (v *Viper) WatchConfig() { initWG := sync.WaitGroup{} initWG.Add(1) go func() { watcher, err := fsnotify.NewWatcher() if err != nil { v.logger.Error(fmt.Sprintf(\u0026#34;failed to create watcher: %s\u0026#34;, err)) os.Exit(1) } defer watcher.Close() // we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way filename, err := v.getConfigFile() if err != nil { v.logger.Error(fmt.Sprintf(\u0026#34;get config file: %s\u0026#34;, err)) initWG.Done() return } configFile := filepath.Clean(filename) configDir, _ := filepath.Split(configFile) realConfigFile, _ := filepath.EvalSymlinks(filename) eventsWG := sync.WaitGroup{} eventsWG.Add(1) go func() { for { select { case event, ok := \u0026lt;-watcher.Events: if !ok { // \u0026#39;Events\u0026#39; channel is closed eventsWG.Done() return } currentConfigFile, _ := filepath.EvalSymlinks(filename) // we only care about the config file with the following cases: // 1 - if the config file was modified or created // 2 - if the real path to the config file changed (eg: k8s ConfigMap replacement) if (filepath.Clean(event.Name) == configFile \u0026amp;\u0026amp; (event.Has(fsnotify.Write) || event.Has(fsnotify.Create))) || (currentConfigFile != \u0026#34;\u0026#34; \u0026amp;\u0026amp; currentConfigFile != realConfigFile) { realConfigFile = currentConfigFile err := v.ReadInConfig() if err != nil { v.logger.Error(fmt.Sprintf(\u0026#34;read config file: %s\u0026#34;, err)) } if v.onConfigChange != nil { v.onConfigChange(event) } } else if filepath.Clean(event.Name) == configFile \u0026amp;\u0026amp; event.Has(fsnotify.Remove) { eventsWG.Done() return } case err, ok := \u0026lt;-watcher.Errors: if ok { // \u0026#39;Errors\u0026#39; channel is not closed v.logger.Error(fmt.Sprintf(\u0026#34;watcher error: %s\u0026#34;, err)) } eventsWG.Done() return } } }() watcher.Add(configDir) initWG.Done() // done initializing the watch in this go routine, so the parent routine can move on... eventsWG.Wait() // now, wait for event loop to end in this go-routine... }() initWG.Wait() // make sure that the go routine above fully ended before returning } 其实流程是相似的：\n首先，调用NewWatcher创建一个监听器； 调用v.getConfigFile()获取配置文件路径，抽出文件名、目录，配置文件如果是一个符号链接，获得链接指向的路径； 调用watcher.Add(configDir)监听配置文件所在目录，另起一个 goroutine 处理事件。 WatchConfig不能阻塞主 goroutine，所以创建监听器也是新起 goroutine 进行的。代码中有两个sync.WaitGroup变量，initWG是为了保证监听器初始化， eventsWG是在事件通道关闭，或配置被删除了，或遇到错误时退出事件处理循环。\n然后就是核心事件循环：\n有事件发生时，判断变化的文件是否是在 viper 中设置的配置文件，发生的是否是创建或修改事件（只处理这两个事件）； 如果配置文件为符号链接，若符合链接的指向修改了，也需要重新加载配置； 如果需要重新加载配置，调用v.ReadInConfig()读取新的配置； 如果注册了事件回调，以发生的事件为参数执行回调。 总结 fsnotify的接口非常简单直接，所有系统相关的复杂性都被封装起来了。这也是我们平时设计模块和接口时可以参考的案例。\n参考 fsnotify API 设计 fsnotify GitHub 仓库 原文：Go 每日一库之 fsnotify ","date":"2024-05-08T15:49:50+08:00","permalink":"https://arlettebrook.github.io/p/fsnotify-introduction/","title":"Fsnotify Introduction"},{"content":" 开源协议(开源许可证)是每一个想要做开源软件的开发者都需要了解的，即使你不想做开源软件，那么当你使用他人开源的软件时也需要了解一些开源协议相关的内容，这样能够尽量避免一些不必要的麻烦。\n什么是开源 开源即开放源代码，是 OSI (Open Source Initiative) 这个组织提出来的。而被开源的软件，我们通常称为开源软件。你可能还见到过 Free Software 一词，它代表 自由软件 而非 免费软件，是开源软件的前身。\n开源软件 = 开放源代码 + 开源协议，一份没有添加开源协议的开源代码，并不是真正的开源软件，也就不能随意使用。\n注意：如果你在 GitHub 上创建了一个没有开源协议的公共代码仓库，其他用户仍然有权查看并为其创建分支，这是由 GitHub 的服务条款决定的。\n开源许可证 开源协议是指开源软件所携带的一份声明协议，这份协议也叫开源许可证。开源许可证声明了开源协议的内容，规定了原作者和使用者的权利以及义务。\n开源许可证是开源软件生态系统的基础，可以促进软件的协同开发。\n开源许可证是具有法律效力的，并且需要得到 OSI 这个组织的认证，目前 OSI 共计认证了 110+ 个开源许可证，这些被认证的开源许可证都必须遵循 OSD (Open Source Definition) 规则。\n虽然开源许可证非常多，但常用的就那么几种。常见的开源许可证主要有 Apache、MIT、BSD、GPL、LGPL、MPL、SSPL 等。\n开源许可证分成两大类：宽松型许可证（Permissive Licenses）、著作权型许可证（反版权许可证）（Copyleft Licenses）。\n​\t宽松型许可证（Permissive Licenses）是一种对软件的使用、修改、传播等方式采用最低限制的自由软件许可协议条款类型。这种类型的软件许可协议将不保证原作品的派生作品会继续保持与原作品完全相同的相关限制条件，从而为原作品的自由使用、修改和传播等提供更大的空间。\n​\t著作权型许可证（Copyleft Licenses）是经原作者许可在有限空间内的自由使用、修改和传播，且不得违背原作品的限制条款。如果一款软件使用 Copyleft 类型许可协议规定软件不得用于商业用途，且不得闭源，那么后续的衍生子软件也必须得遵循该条款。\n两者最大的差别在于：在软件被修改并再发行时， Copyleft License 仍然强制要求公开源代码（衍生软件需要开源），而 Permissive licence 不要求公开源代码（衍生软件可以变为专有软件）。\n其中，Apache、MIT、BSD 都是宽松型许可证，GPL 是典型的著作权型（copyleft ）许可证，LGPL、MPL 是弱著作权型（copyleft ）许可证。SSPL 则是近年来 MongoDB 创建的一个新许可证，存在较大争议，开放源代码促进会 OSI 甚至认为 SSPL 就不是开源许可协议。\n此外，还有一类是 Creative Commons（CC）知识共享协议。严格意义上说该协议并不能说是真正的开源协议，它们大多是被使用于设计类的工程上。CC 协议种类繁多，每一种都授权特定的权利。大多数的比较严格的 CC 协议会声明 “署名权，非商业用途，禁止衍生” 条款，这意味着你可以自由的分享这个作品，但你不能改变它和对其收费，而且必须声明作品的归属。这个许可协议非常的有用，它可以让你的作品传播出去，但又可以对作品的使用保留部分或完全的控制。最少限制的 CC 协议类型当属 “署名” 协议，这意味着只要人们能维护你的名誉，他们对你的作品怎么使用都行。\n下面就主要介绍下几种常见开源许可证。\n宽松型许可证（Permissive Licenses） 顾名思义，这类开源许可证比较宽松，限制更少。常见宽松开源许可证有：\nBSD (2-Clause) (Berkeley Software Distribution，伯克利软件发行版)\n源代码或二进制形式的重新分发，必须保留原始的许可证声明。\nBSD (3-Clause)\n在 BSD(2-Clause) 基础上增加了一条，禁止使用原始作者的名字为衍生软件进行促销。\nGo 语言就在使用 BSD (3-Clause) 开源许可证。\nMIT (Massachusetts Institute of Technology，麻省理工学院许可证)\n免费授予任何人该软件及相关文档的权限，包括但不限于使用、复制、修改、合并、发表、分发、再授权、出售软件的副本。分发软件时，必须保留原始的许可证声明。\nMIT 是最为宽松的开源许可证，所以这也使得它成为最流行的开源许可证，如目前在前端领域非常有名的 Vue.js 就在使用它。\nApache-2.0 (Apache 软件基金会发布的许可证)\nApache 许可证内容非常多，不过可以简单的总结几点：\n分发软件时，必须保留原始的许可证声明。\n所有修改过的文件，必须加以说明告知用户此文件已被更改。\n没有修改过的文件，不得修改许可证。\n云原生领域著名软件 Kubernetes 使用的正是 Apache-2.0 开源许可证。\n著作权型许可证（Copyleft Licenses） Copyleft 一词由 理查德·斯托曼 发明，表示 Copyright (版权) 的反义词。Copyleft 表示不经许可，用户无权复制，商业软件开发人员通过 Copyleft剥夺了用户的自由。Copyright 则表示不经许可，用户有权复制，Copyright使用版权来给予用户自由。\n因此Copyleft 类的许可证要比 Permissive 许可证限制更多。注意：不一定要经过原作者许可。常见 Copyleft 开源许可证有：\nGPL (GNU General Public License)\nGPL 有两个版本，GPL-2.0 和 GPL-3，同 BSD 一样，更高版本会带来更多的限制。GPL 协议内容也非常多，我们最需要关注的一点是：使用了 GPL 协议的开源软件，其衍生软件如果需要分发，就必须开源并且同样要使用此协议。\n由于这条规定的存在，有人甚至把 GPL 协议称为 “GPL 病毒”，因为它具有跟病毒一样的传染性。不过 GPL 仍然是非常流行的开源许可证，比如大名鼎鼎的 Linux 就采用了 GPL 协议。\nGPL 是流行开源许可证中最为严格的，所以对于使用开源软件所衍化的商业化软件就不够友好了。\nLGPL (GNU Library General Public License)\n算是 GPL 的一个变种，主要为类库使用而设计的开源协议。\n商用软件如果采用类库方式引用使用了 LGPL 协议的开源软件，则可以不用开源。\n如果是修改或衍生软件需要分发，则必须开源并且同样要使用此协议。这点与 GPL 协议一样。\nAGPL (GNU Affero General Public License)\n除了 AGPL 许可证，上面介绍的其他许可证的限制条件都是只有在分发时才需要遵守。而 SaaS 软件作为一项云服务则不构成分发，所以可以不遵守这些许可证条款。\n为了解决这些早期发布的许可证对 SaaS 软件无效的尴尬，GNU 又发布了 AGPL 许可证，它规定如果 SaaS 用到的代码是该许可证，那么其云服务的代码也必须开源。\n国产开源时序数据库 TDengine 为了阻止云厂商免费使用其开源版本，就采用了 AGPL 协议。\nMPL-2.0 (Mozilla Public License 2.0，Mozilla 基金会发布的许可证)\nMPL 融合了 BSD 开源许可证 和 GPL 开源许可证 的特性，力争在专有软件和开源软件开发者之间寻求平衡。是比 BSD 更严格，比 GPL 更宽松的开源许可证。\nMPL 允许新增的独立代码文件闭源，但在 MPL 授权下的代码文件必须保持 MPL 授权且开源。这使得 MPL 既不像 MIT 和 BSD 那样允许派生作品完全转化为闭源，也不像 GPL 那样要求所有的派生作品，包括新的组件在内，必须全部保持使用 GPL。\nMozilla 自家的 Firefox 浏览器就使用此开源许可证。\n以上介绍的开源许可证都是较为常见的许可证，另外还有两个不太常见但却值得一提的许可证。\n一个是 Unlicense 许可证，翻译过来叫「零约束许可证」，可以说是被 OSI 认证的最开放的许可证了。根据名字也能看出来，Unlicense 是一个不包含任何约束条件的许可证，专用于贡献作品到公共领域。任何人都可以对开源软件进行自由复制、修改、发布、使用、编译、出售等，并且可用于任何商业或非商业目的。\n另外，在 OSI 官网公布的开源许可证列表中，还有一个叫「木兰（Mulan PSL2）」的开源许可证，它是中国本土唯一获得 OSI 认可的开源许可证。Mulan PSL2 以中英文双语表述，中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致，以中文版为准。\n「木兰」并不是一个许可证，而是一系列许可证，它包含木兰宽松许可证、木兰公共许可证、木兰开放作品许可协议。其中木兰宽松许可证第 2 版（Mulan PSL2）在 2020 年 2 月 14 日通过 OSI 批准。\n如果你想使用一个中文的开源许可证，那么 Mulan PSL2 目前是你唯一的选择。\n使用开源许可证 以 MIT 为例，我们来学习下如何在自己的开源项目中使用开源许可证：\n首先我们需要在自己的开源项目根目录下创建一个叫 LICENSE 的文本文件，注意文件名不包含任何后缀。\n然后去到 OSI 官网找到 MIT 开源许可证模板，内容如下：\n1 2 3 4 5 6 7 Copyright \u0026lt;YEAR\u0026gt; \u0026lt;COPYRIGHT HOLDER\u0026gt; Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 将开源许可证模板内容复制到 LICENSE 文本文件中，并将第一行 Copyright 后面的 \u0026lt;YEAR\u0026gt; 替换为当前年份，将 \u0026lt;COPYRIGHT HOLDER\u0026gt; 替换为自己的名字。\n当开放项目源代码时，将此文件一同开放出去即可。如果你使用 GitHub 开放源码，则只需要将此 LICENSE 加入到 git 管理即可。\n如果你是在 GitHub 上新建开源项目，在创建项目界面，有一个 Choose a license 按钮可以很方便的选择一款开源协议，并且 GitHub 会自动替换许可证模板中的年份、作者等信息。 另外，我们在开放源代码时，其实可以不使用 OSI 认证的开源许可证，而是选择自己写一份许可证，用来声明版权。这同样是具有法律效力的，不过这份许可证就不能叫做开源许可证了。 如何选择开源许可证 乌克兰程序员 Paul Bagwell 画了一张图在网上很是流行，阮一峰老师将其翻译成了中文，如下：\n注：\n关于什么是许可证兼容性可以参考：《开源许可证兼容性指南.docx》 从上图中可以看出，大体上左边的许可证比较严格，右边的许可证较为宽松。此图虽然不够严谨，胜在方便理解。在开源自己的项目时，可以根据此图快速选择出适合自己的开源许可证。\n更多参考图：\n知识共享许可证Creative Commons（CC） 有时候，我们想要开源的并不是一款软件，而是一套开源的教程或者书籍等，此时严格来讲并不能使用上面所介绍的开源许可证。\n在 OSD 第 2 条中有规定：开源软件是必须要包含源代码的。\n也就是说，教程或者书籍等没有源代码，并不能作为开源软件，也就不能使用开源许可证。\n此类项目想要开源，应该使用「知识共享许可证」（creative commons licenses），通常也叫 CC 许可证。\nCC 许可证由 Creative Commons 基金会提出，虽然没有得到 OSI 的认可，但他仍具有法律效力，并且应用广泛。\n上面提到的「木兰开放作品许可协议」就是对标知识共享许可证的。同木兰许可证类似，知识共享许可证也是一系列许可证，目前最新的知识共享许可证为 4.0 版本，常见的许可证有 6 种：\nCC BY 4.0 (Attribution 4.0 International，署名 4.0 国际) CC BY-SA 4.0 (Attribution-ShareAlike 4.0 International，署名-相同方式共享 4.0 国际) CC BY-ND 4.0 (Attribution-NoDerivatives 4.0 International，署名-禁止演绎 4.0 国际) CC BY-NC 4.0 (Attribution-NonCommercial 4.0 International，署名-非商业性使用 4.0 国际) CC BY-NC-SA 4.0 (Attribution-NonCommercial-ShareAlike 4.0 International，署名-非商业性使用-相同方式共享 4.0 国际) CC BY-NC-ND 4.0 (Attribution-NonCommercial-NoDerivatives 4.0 International，署名-非商业性使用-禁止演绎 4.0 国际) 可以发现，CC 许可证命名方式就是它的权利简拼组合。以下是对其中出现的几个名词的解释：\n​\t署名：必须给出原作者的署名，提供指向本许可协议的链接，同时标明是否对原始作品作了修改。\n​\t非商业性使用：您不得将本作品用于商业目的。不得用于盈利性目的。\n​\t相同方式共享：在任何媒介以任何形式复制、发行本作品时必须采用相同的许可证。\n​\t禁止演绎：禁止修改、转换或以本作品为基础进行创作。\n之所以每个许可证后面都带有国际两个字，是因为这系列许可证发布了不同的地域版，不过国际版更为通用。\n需要注意 CC 系列许可证一旦发布，就不可收回，只要你遵守许可协议条款，许可人就无法收回你的这些权利。\n如需使用 CC 许可证，可以参考示例。\n​\t本教程采用知识共享 署名-相同方式共享 4.0国际协议。\n​\t博客内容遵循 知识共享 署名 - 非商业性 - 相同方式共享 4.0 国际协议\n或参考官网自行组合。\n注意：协议链接并不必须指定，但在使用知识共享协议时，提供协议链接是一个很好的做法，因为它可以帮助其他人方便地访问并了解你所使用的具体协议内容。\n开源案例 介绍完了开源协议，我们再来看一个开源案例：\n中国首例因违反 GPL 协议致侵害计算机软件著作权纠纷 2021-06-30 在中国裁判文书网上公布了一则民事判决书，标题为：「济宁市罗盒网络科技有限公司诉被告福建风灵创景科技有限公司(以下简称福建风灵公司)、被告北京风灵创景科技有限公司(以下简称北京风灵公司)、被告深圳市腾讯计算机系统有限公司(以下简称腾讯公司)侵害计算机软件著作权纠纷一审民事判决书」。案件概况如下：\n原告济宁市罗盒网络科技有限公司独立开「罗盒（VirtualApp）」从 2016 年 7 月 8 日的版本开始引入开源协议，起初为 LGPL3.0 协议，从 2016 年 8 月 12 日开始更换为 GPL3.0 协议。2017 年 10 月 29 日开始删除适用 GPL3.0 协议的表述，但英文介绍中仍保留openplatform 的表述。\n2018 年 9 月，原告调查发现名为「点心桌面」的软件使用了 VirtualApp 的代码，将两个软件源代码进行分析比对，两者间 421 个可比代码中有 308 个代码具有实质相似性，有 27 个代码具有高度相似性，有 78 个代码具有一般相似性。因此，被诉侵权软件与涉案软件构成实质相似。\n经查，被诉「点心桌面」中使用了原告采用 GPL3.0 协议发布的 VirtualApp，被告对此亦予以确认。\n原告申请赔偿 2000 万，最终，法院酌情确定赔偿数额为 50 万元。原告为制止本案侵权行为所支出的合理费用，计算在赔偿损失数额范围之内。\n更多细节可以点击下载查看。\n该案例给开源软件使用者敲响一记警钟，使用开源软件一定要查看并遵循开源许可证。\n总结 本文带大家一起认识了什么是开源协议，并且还对常用开源协议进行了分析，以及如何使用开源协议。同时讲解了针对书籍等开源作品使用的知识共享许可协议和使用方式。最终分享了一个开源软件纠纷案例，以说明了解开源协议的重要性。\n此文仅为作者本人学习并整理的开源协议相关知识，即不够全面，也不够严谨，不能作为法律依据。希望你能通过本篇文章认识并重视开源协议，学习和书写本篇文章时间有限，难免出现表达不够准确或错误的地方，欢迎批评指正。\n最后，想提醒大家，身为一名开发者，掌握开源协议是有必要的。不过开源协议的内容非常多且专业，想要完全了解也是一项繁重的工作，毕竟这不是我们的专业领域，如果遇到无法确定的问题，可以寻求身边的专业法务帮忙。\n参考 各种开源协议介绍 一文看懂开源许可证丨开源知识科普 原文：开源协议简介 如何选择开源许可证？ ","date":"2024-05-06T23:22:29+08:00","permalink":"https://arlettebrook.github.io/p/open-source-license-introduction/","title":"Open Source License Introduction"},{"content":" Viper 是一个功能齐全的 Go 应用程序配置库，支持很多场景。它可以处理各种类型的配置需求和格式，包括设置默认值、从多种配置文件和环境变量中读取配置信息、实时监视配置文件等。无论是小型应用还是大型分布式系统，Viper 都可以提供灵活而可靠的配置管理解决方案。在本文中，我们将深入探讨 Viper 的各种用法和使用场景，以帮助读者更好地了解和使用 Viper 来管理应用程序配置。\n为什么选择 Viper 当我们在做技术选型时，肯定要知道为什么选择某一项技术，而之所以选择使用 Viper 来管理应用程序的配置，Viper 官方给出了如下答案：\n当构建应用程序时，你不想担心配置文件格式，只想专注于构建出色的软件。Viper 就是为了帮助我们解决这个问题而存在的。\nViper 可以完成以下工作：\n查找、加载和反序列化 JSON、TOML、YAML、HCL、INI、envfile 或 Java Properties 等多种格式的配置文件。 为不同的配置选项设置默认值。 为通过命令行标志指定的选项设置覆盖值。 提供别名系统，以便轻松重命名配置项而不破坏现有代码。 可以轻松区分用户提供的命令行参数或配置文件中的值是否与默认值相同。 可以设置监听配置文件的修改，修改时自动加载新的配置； 从环境变量、命令行选项和io.Reader中读取配置； 从远程配置系统中读取和监听修改，如 etcd/Consul； 代码逻辑中显示设置键值。 \u0026hellip;\u0026hellip; 注：关于上面第 5 点，我个人理解的使用场景是：\n先从命令行参数或配置文件中读取配置。 可以使用 viper.IsSet(key) 方法判断用户是否设置了 key 所对应的 value，如果设置了，可以通过 viper.Get(key) 获取值。 调用 viper.SetDefault(key, default_value) 来设置默认值（默认值不会覆盖上一步所获取到的值）。 在第 2 步中可以拿到用户设置的值 value，在第 3 步中可以知道默认值 default_value，这样其实就可以判断两者是否相同了。 Viper 采用以下优先级顺序来加载配置，按照优先级由高到低排序如下：\n显式调用 viper.Set 设置的配置值 命令行参数 环境变量 配置文件 key/value 存储 默认值 注意 ⚠️：Viper 配置中的键不区分大小写，如 user/User/USER 被视为是相等的 key，关于是否将其设为可选，目前还在讨论中。\nViper 包中最核心的两个功能是：如何把配置值读入 Viper 和从 Viper 中读取配置值，接下来我将分别介绍这两个功能。\n快速使用 安装：\n1 go get -u github.com/spf13/viper 导入：\n1 import \u0026#34;github.com/spf13/viper\u0026#34; 使用：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetConfigName(\u0026#34;config\u0026#34;) viper.SetConfigType(\u0026#34;toml\u0026#34;) viper.AddConfigPath(\u0026#34;.\u0026#34;) viper.SetDefault(\u0026#34;redis.port\u0026#34;, 6381) err := viper.ReadInConfig() if err != nil { log.Fatalf(\u0026#34;read config failed: %v\u0026#34;, err) } fmt.Println(viper.Get(\u0026#34;app_name\u0026#34;)) fmt.Println(viper.Get(\u0026#34;log_level\u0026#34;)) fmt.Println(\u0026#34;mysql ip: \u0026#34;, viper.Get(\u0026#34;mysql.ip\u0026#34;)) fmt.Println(\u0026#34;mysql port: \u0026#34;, viper.Get(\u0026#34;mysql.port\u0026#34;)) fmt.Println(\u0026#34;mysql user: \u0026#34;, viper.Get(\u0026#34;mysql.user\u0026#34;)) fmt.Println(\u0026#34;mysql password: \u0026#34;, viper.Get(\u0026#34;mysql.password\u0026#34;)) fmt.Println(\u0026#34;mysql database: \u0026#34;, viper.Get(\u0026#34;mysql.database\u0026#34;)) fmt.Println(\u0026#34;redis ip: \u0026#34;, viper.Get(\u0026#34;redis.ip\u0026#34;)) fmt.Println(\u0026#34;redis port: \u0026#34;, viper.Get(\u0026#34;redis.port\u0026#34;)) } 这里的配置文件格式是toml。toml 的语法很简单，快速入门请看TOML:简体中文。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # config.toml app_name = \u0026#34;awesome web\u0026#34; # possible values: DEBUG, INFO, WARNING, ERROR, FATAL log_level = \u0026#34;DEBUG\u0026#34; [mysql] ip = \u0026#34;127.0.0.1\u0026#34; port = 3306 user = \u0026#34;dj\u0026#34; password = 123456 database = \u0026#34;awesome\u0026#34; [redis] ip = \u0026#34;127.0.0.1\u0026#34; port = 7381 viper 的使用非常简单，它需要很少的设置。设置文件名（SetConfigName）、配置类型（SetConfigType）和搜索路径（AddConfigPath），然后调用ReadInConfig。 viper会自动根据类型来读取配置。使用时调用viper.Get方法获取键值。\n编译、运行程序：\n1 2 3 4 5 6 7 8 9 10 $ go run main.go awesome web DEBUG mysql ip: 127.0.0.1 mysql port: 3306 mysql user: dj mysql password: 123456 mysql database: awesome redis ip: 127.0.0.1 redis port: 7381 有几点需要注意：\n设置文件名时不要带后缀； 搜索路径可以设置多个，viper 会根据设置顺序依次查找； viper 获取值时使用section.key的形式，即传入嵌套的键名； 默认值可以调用viper.SetDefault设置。 读取键 viper 提供了多种形式的读取方法。在上面的例子中，我们看到了Get方法的用法。Get方法返回一个interface{}的值，使用有所不便。\nGetType系列方法可以返回指定类型的值。 其中，Type 可以为Bool/Float64/Int/String/Time/Duration/IntSlice/StringSlice。 但是请注意，如果指定的键不存在或类型不正确，GetType方法返回对应类型的零值。\n如果要判断某个键是否存在，使用IsSet方法。 另外，GetStringMap和GetStringMapString直接以 map 返回某个键下面所有的键值对，前者返回map[string]interface{}，后者返回map[string]string。 AllSettings以map[string]interface{}返回所有设置。\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 // 省略包名和 import 部分 func main() { viper.SetConfigName(\u0026#34;config\u0026#34;) viper.SetConfigType(\u0026#34;toml\u0026#34;) viper.AddConfigPath(\u0026#34;.\u0026#34;) err := viper.ReadInConfig() if err != nil { log.Fatalf(\u0026#34;read config failed: %v\u0026#34;, err) } fmt.Println(\u0026#34;protocols: \u0026#34;, viper.GetStringSlice(\u0026#34;server.protocols\u0026#34;)) fmt.Println(\u0026#34;ports: \u0026#34;, viper.GetIntSlice(\u0026#34;server.ports\u0026#34;)) fmt.Println(\u0026#34;timeout: \u0026#34;, viper.GetDuration(\u0026#34;server.timeout\u0026#34;)) fmt.Println(\u0026#34;mysql ip: \u0026#34;, viper.GetString(\u0026#34;mysql.ip\u0026#34;)) fmt.Println(\u0026#34;mysql port: \u0026#34;, viper.GetInt(\u0026#34;mysql.port\u0026#34;)) if viper.IsSet(\u0026#34;redis.port\u0026#34;) { fmt.Println(\u0026#34;redis.port is set\u0026#34;) } else { fmt.Println(\u0026#34;redis.port is not set\u0026#34;) } fmt.Println(\u0026#34;mysql settings: \u0026#34;, viper.GetStringMap(\u0026#34;mysql\u0026#34;)) fmt.Println(\u0026#34;redis settings: \u0026#34;, viper.GetStringMap(\u0026#34;redis\u0026#34;)) fmt.Println(\u0026#34;all settings: \u0026#34;, viper.AllSettings()) } 我们在配置文件 config.toml 中添加protocols和ports配置：\n1 2 3 4 [server] protocols = [\u0026#34;http\u0026#34;, \u0026#34;https\u0026#34;, \u0026#34;port\u0026#34;] ports = [10000, 10001, 10002] timeout = \u0026#34;3s\u0026#34; 编译、运行程序，输出：\n1 2 3 4 5 6 7 8 9 10 11 12 $ go run main.go protocols: [http https port] ports: [10000 10001 10002] timeout: 3s mysql ip: 127.0.0.1 mysql port: 3306 redis.port is set mysql settings: map[database:awesome ip:127.0.0.1 password:123456 port:3306 user:dj] redis settings: map[ip:127.0.0.1 port:7381] all settings: map[app_name:awesome web log_level:DEBUG mysql:map[database:awesome ip:127.0.0.1 pa ssword:123456 port:3306 user:dj] redis:map[ip:127.0.0.1 port:7381] server:map[ports:[10000 10001 1 0002] protocols:[http https port] timeout:3s]] 如果将配置中的redis.port注释掉，将输出redis.port is not set。\n上面的示例中还演示了如何使用time.Duration类型，只要是time.ParseDuration接受的格式都可以，例如3s、2min、1min30s等。\n设置键值 viper 支持在多个地方设置，使用下面的顺序依次读取：\n调用Set显示设置的； 命令行选项； 环境变量； 配置文件； 默认值。 viper.Set\n如果某个键通过viper.Set设置了值，那么这个值的优先级最高。\n1 viper.Set(\u0026#34;redis.port\u0026#34;, 5381) 如果将上面这行代码放到程序中，运行程序，输出的redis.port将是 5381。\nviper.SetDefault\n设置默认值，如果没有配置键，将使用默认值\n1 viper.SetDefault(\u0026#34;log_level\u0026#34;, \u0026#34;INFO\u0026#34;) 如果配置文件中没有配置log_level,那么将使用INFO\n把配置值读入Viper Viper 支持多种方式读入配置：\n设置默认配置值 从配置文件读取配置 监控并重新读取配置文件 从 io.Reader 读取配置 从环境变量读取配置 从命令行参数读取配置 从远程 key/value 存储读取配置 我们一个一个来看。\n设置默认配置值 一个好的配置系统应该支持默认值。Viper 支持使用 viper.SetDefault(key, value) 为 key 设置默认值 value，在没有通过配置文件、环境变量、远程配置或命令行标志设置 key 所对应值的情况下，这很有用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { // 设置默认配置 viper.SetDefault(\u0026#34;username\u0026#34;, \u0026#34;arlettebrook\u0026#34;) viper.SetDefault(\u0026#34;server\u0026#34;, map[string]string{\u0026#34;ip\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;port\u0026#34;: \u0026#34;8080\u0026#34;}) // 读取配置值 fmt.Printf(\u0026#34;username: %s\\n\u0026#34;, viper.Get(\u0026#34;Username\u0026#34;)) // key 不区分大小写 fmt.Printf(\u0026#34;server: %+v\\n\u0026#34;, viper.Get(\u0026#34;server\u0026#34;)) } 执行以上示例代码得到如下输出：\n1 2 3 $ go run main.go username: arlettebrook server: map[ip:127.0.0.1 port:8080] 从配置文件读取配置 Viper 支持从 JSON、TOML、YAML、HCL、INI、envfile 或 Java Properties 格式的配置文件中读取配置。Viper 可以搜索多个路径，但目前单个 Viper 实例只支持单个配置文件。Viper 不会默认配置任何搜索路径，将默认决定留给应用程序。\n主要有两种方式来加载配置文件：\n通过 viper.SetConfigFile() 指定配置文件，显式定义配置文件的路径、名称和扩展名。 Viper将使用它并且不检查任何配置路径。 通过 viper.SetConfigName() 指定不带扩展名的配置文件，viper.SetConfigType()指定配置文件类型类型。然后通过 viper.AddConfigPath() 指定配置文件的搜索路径中，可以通过多次调用，来设置多个配置文件搜索路径。Viper 会根据所添加的路径顺序查找指定配置文件，如果找到就停止查找。 1 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 package main import ( \u0026#34;errors\u0026#34; \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) var ( cfg = flag.String(\u0026#34;c\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;config file.\u0026#34;) ) func main() { flag.Parse() if *cfg != \u0026#34;\u0026#34; { viper.SetConfigFile(*cfg) // 指定配置文件（路径 + 配置文件名） } else { viper.AddConfigPath(\u0026#34;.\u0026#34;) // 把当前目录加入到配置文件的搜索路径中 viper.AddConfigPath(\u0026#34;$HOME/.config\u0026#34;) // 可以多次调用 AddConfigPath 来设置多个配置文件搜索路径 viper.SetConfigName(\u0026#34;config\u0026#34;) viper.SetConfigType(\u0026#34;toml\u0026#34;) // 如果配置文件名中没有扩展名，则需要显式指定配置文件的格式// 指定配置文件名（没有扩展名） } // 读取配置文件 if err := viper.ReadInConfig(); err != nil { var configFileNotFoundError viper.ConfigFileNotFoundError if errors.As(err, \u0026amp;configFileNotFoundError) { log.Fatalln(configFileNotFoundError.Error()) } log.Fatalln(err) } fmt.Printf(\u0026#34;using config file: %s\\n\u0026#34;, viper.ConfigFileUsed()) // 读取配置值 fmt.Printf(\u0026#34;username: %s\\n\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) } viper.ConfigFileUsed()返回使用的配置文件的路径\n假如有如下配置文件 config.yaml 与示例程序在同一目录中：\n1 2 3 4 5 6 # config.yaml username: arlettebrook password: 123456 server: ip: 127.0.0.1 port: 8080 执行以上示例代码得到如下输出：\n1 2 3 $ go run main.go -c ./config.yaml using config file: ./config.yaml username: arlettebrook 监控并重新读取配置文件 Viper 支持在应用程序运行过程中实时读取配置文件，即热加载配置。\n只需要调用 viper.WatchConfig() 即可开启此功能。\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/fsnotify/fsnotify\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetConfigFile(\u0026#34;./config.yaml\u0026#34;) // 注册每次配置文件发生变更后都会调用的回调函数 viper.OnConfigChange(func(e fsnotify.Event) { fmt.Printf(\u0026#34;config file changed: %s username:%s\\n\u0026#34;, e.Name, viper.Get(\u0026#34;username\u0026#34;)) }) // 监控并重新读取配置文件，需要确保在调用前添加了所有的配置路径 viper.WatchConfig() err := viper.ReadInConfig() if err != nil { log.Fatalln(\u0026#34;加载配置文件错误:\u0026#34;, err) } // 读取配置值 fmt.Printf(\u0026#34;未修改的username: %s\\n\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) // 阻塞程序，这个过程中可以手动去修改配置文件内容，观察程序输出变化,注意要保存。 time.Sleep(time.Second * 10) // 读取配置值 fmt.Printf(\u0026#34;最终的username: %s\\n\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) } 值得注意的是，在调用 viper.WatchConfig() 监控并重新读取配置文件之前，需要确保添加了所有的配置搜索路径。\n并且，我们还可以通过 viper.OnConfigChange() 函数注册一个每次配置文件发生变更后都会调用的回调函数。\n我们依然使用上面的 config.yaml 配置文件：\n1 2 3 4 5 username: arlettebrook password: 123456 server: ip: 127.0.0.1 port: 8080 执行以上示例代码，并在程序阻塞的时候，手动修改配置文件中 username 后面分别追加1保存、2保存3保存，可以得到如下输出：\n1 2 3 4 5 6 7 8 9 $ go run main.go 未修改的username: arlettebrook config file changed: config.yaml username:arlettebrook1 config file changed: config.yaml username:arlettebrook1 config file changed: config.yaml username:arlettebrook12 config file changed: config.yaml username:arlettebrook12 config file changed: config.yaml username:arlettebrook123 config file changed: config.yaml username:arlettebrook123 最终的username: arlettebrook123 我这里修改一次回调函数不知道为什么执行了俩次，不过没有影响。\n监听文件修改 viper 可以监听文件修改，热加载配置。因此不需要重启服务器，就能让配置生效。\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetConfigName(\u0026#34;config\u0026#34;) viper.SetConfigType(\u0026#34;toml\u0026#34;) viper.AddConfigPath(\u0026#34;.\u0026#34;) err := viper.ReadInConfig() if err != nil { log.Fatalf(\u0026#34;read config failed: %v\u0026#34;, err) } viper.WatchConfig() fmt.Println(\u0026#34;redis port before sleep: \u0026#34;, viper.Get(\u0026#34;redis.port\u0026#34;)) time.Sleep(time.Second * 10) fmt.Println(\u0026#34;redis port after sleep: \u0026#34;, viper.Get(\u0026#34;redis.port\u0026#34;)) } 只需要调用viper.WatchConfig，viper 会自动监听配置修改。如果有修改，重新加载的配置。\n上面程序中，我们先打印redis.port的值，然后Sleep 10s。在这期间修改配置中redis.port的值，Sleep结束后再次打印。 发现打印出修改后的值：\n1 2 redis port before sleep: 7381 redis port after sleep: 73810 另外，还可以为配置修改增加一个回调：\n1 2 3 viper.OnConfigChange(func(e fsnotify.Event) { fmt.Printf(\u0026#34;Config file:%s Op:%s\\n\u0026#34;, e.Name, e.Op) }) 这样文件修改时会执行这个回调。\nviper 使用fsnotify这个库来实现监听文件修改的功能。\n从 io.Reader 读取配置 Viper 支持从任何实现了 io.Reader 接口的配置源中读取配置。注意需要指定配置文件的类型，才能识别io.Reader。\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 package main import ( \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetConfigType(\u0026#34;yaml\u0026#34;) // 或者使用 viper.SetConfigType(\u0026#34;YAML\u0026#34;) var yamlExample = []byte(` username: arlettebrook password: 123456 server: ip: 127.0.0.1 port: 8080 `) err := viper.ReadConfig(bytes.NewBuffer(yamlExample)) if err != nil { log.Fatalln(\u0026#34;读取配置文件错误：\u0026#34;, err) } // 读取配置值 fmt.Printf(\u0026#34;username: %s\\n\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) } 这里我们通过 bytes.NewBuffer() 构造了一个 bytes.Buffer 对象，它实现了 io.Reader 接口，所以可以直接传递给 viper.ReadConfig() 来从中读取配置。\n执行以上示例代码得到如下输出：\n1 2 $ go run main.go username: arlettebrook 从io.Reader中读取 viper 支持从io.Reader中读取配置。这种形式很灵活，来源可以是文件，也可以是程序中生成的字符串，甚至可以从网络连接中读取的字节流。\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 package main import ( \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetConfigType(\u0026#34;toml\u0026#34;) tomlConfig := []byte(` app_name = \u0026#34;awesome web\u0026#34; # possible values: DEBUG, INFO, WARNING, ERROR, FATAL log_level = \u0026#34;DEBUG\u0026#34; [mysql] ip = \u0026#34;127.0.0.1\u0026#34; port = 3306 user = \u0026#34;dj\u0026#34; password = 123456 database = \u0026#34;awesome\u0026#34; [redis] ip = \u0026#34;127.0.0.1\u0026#34; port = 7381 `) err := viper.ReadConfig(bytes.NewBuffer(tomlConfig)) if err != nil { log.Fatalf(\u0026#34;read config failed: %v\u0026#34;, err) } fmt.Println(\u0026#34;redis port: \u0026#34;, viper.GetInt(\u0026#34;redis.port\u0026#34;)) } 从环境变量读取配置 Viper 还支持从环境变量读取配置，有 5 个方法可以帮助我们使用环境变量:\nAutomaticEnv()：使Viper检查环境变量是否与任何现有键（配置、默认值或标志）匹配。如果找到匹配的环境变量，它们将被加载到Viper中。\n开启自动匹配（根据前缀匹配）环境变量，加载到Viper中。如何没有前缀将加载所有环境变量。 BindEnv(string...) : error：绑定一个环境变量。需要一个或两个参数，第一个参数是配置项的键名（不区分大小写），第二个参数是环境变量的名称。如果未提供第二个参数，则 Viper 将假定环境变量名为：环境变量前缀_键名，且为全大写形式。例如环境变量前缀为 ENV，键名为 username，则环境变量名为 ENV_USERNAME。当显式提供第二个参数时，它不会自动添加前缀，也不会自动将其转换为大写。例如，使用 viper.BindEnv(\u0026quot;username\u0026quot;, \u0026quot;username\u0026quot;) 绑定键名为 username 的环境变量，应该使用 viper.Get(\u0026quot;username\u0026quot;) 读取环境变量的值。\n在使用环境变量时，需要注意，每次访问它的值时都会去环境变量中读取。当调用 BindEnv 时，Viper 不会固定它的值。\nSetEnvPrefix(string)：可以告诉 Viper 在读取环境变量时使用的前缀。BindEnv 和 AutomaticEnv 都将使用此前缀。例如，使用 viper.SetEnvPrefix(\u0026quot;ENV\u0026quot;) 设置了前缀为 ENV，并且使用 viper.BindEnv(\u0026quot;username\u0026quot;) 绑定了环境变量，在使用 viper.Get(\u0026quot;username\u0026quot;) 读取环境变量时，实际读取的 key 是 ENV_USERNAME。\nSetEnvKeyReplacer(string...) *strings.Replacer：允许使用 strings.Replacer 对象在一定程度上重写环境变量的键名。例如，存在 SERVER_IP=\u0026quot;127.0.0.1\u0026quot; 环境变量，使用 viper.SetEnvKeyReplacer(strings.NewReplacer(\u0026quot;.\u0026quot;, \u0026quot;_\u0026quot;, \u0026quot;-\u0026quot;, \u0026quot;_\u0026quot;)) 将键名中的 . 或 - 替换成 _，则通过 viper.Get(\u0026quot;server_ip\u0026quot;)、viper.Get(\u0026quot;server.ip\u0026quot;)、viper.Get(\u0026quot;server-ip\u0026quot;) 三种方式都可以读取环境变量对应的值。\nAllowEmptyEnv(bool)：当环境变量为空时（有键名而没有值的情况），默认会被认为是未设置的，并且程序将回退到下一个配置来源。要将空环境变量视为已设置，可以使用此方法。\nviper.AllSettings()读取全部配置，只能获取到通过 BindEnv 绑定的环境变量，无法获取到通过 AutomaticEnv 绑定的环境变量\n注意 ⚠️：\nViper 在读取环境变量时，是不区分大小写的。如果指定的环境变量与绑定的大小写不一致，viper会自动大小写转换。 以下代码是在windows下的bash终端中运行的。 使用示例：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetEnvPrefix(\u0026#34;env\u0026#34;) // 设置读取环境变量前缀，会自动转为大写 ENV viper.AllowEmptyEnv(true) // 将空环境变量视为已设置 viper.AutomaticEnv() // 开启自动匹配（根据前缀匹配）环境变量，加载到Viper中 _ = viper.BindEnv(\u0026#34;username\u0026#34;) // 也可以单独绑定某一个环境变量 _ = viper.BindEnv(\u0026#34;password\u0026#34;) // 将键名中的 . 或 - 替换成 _ viper.SetEnvKeyReplacer(strings.NewReplacer(\u0026#34;.\u0026#34;, \u0026#34;_\u0026#34;, \u0026#34;-\u0026#34;, \u0026#34;_\u0026#34;)) // 读取配置 fmt.Printf(\u0026#34;username: %v\\n\u0026#34;, viper.Get(\u0026#34;USERNAME\u0026#34;)) fmt.Printf(\u0026#34;password: %v\\n\u0026#34;, viper.Get(\u0026#34;password\u0026#34;)) fmt.Printf(\u0026#34;server.ip: %v\\n\u0026#34;, viper.Get(\u0026#34;server.ip\u0026#34;)) // fmt.Printf(\u0026#34;GOPATH:%v\\n\u0026#34;,viper.Get(\u0026#34;gopath\u0026#34;)) // 请注释到前缀在取消注释运行 // 读取全部配置，只能获取到通过 BindEnv 绑定的环境变量，无法获取到通过 AutomaticEnv 绑定的环境变量 fmt.Println(viper.AllSettings()) } 执行以上示例代码得到如下输出：\n1 2 3 4 5 $ ENV_USERNAME=arlettebrook ENV_SERVER_IP=127.0.0.1 ENV_PASSWORD= go run main.go username: arlettebrook password: server.ip: 127.0.0.1 map[password: username:arlettebrook] 环境变量 如果从命令行参数都没有获取到键值，将尝试从环境变量中读取。我们既可以一个个绑定，也可以自动全部绑定。\n在init方法中调用AutomaticEnv方法绑定全部环境变量：\n1 2 3 4 func init() { // 绑定环境变量 viper.AutomaticEnv() } 为了验证是否绑定成功，我们在main方法中将环境变量 GOPATH 打印出来：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func init() { // 绑定环境变量 viper.AutomaticEnv() } func main() { fmt.Println(\u0026#34;GOPATH:\u0026#34;, viper.Get(\u0026#34;gopath\u0026#34;)) fmt.Println(\u0026#34;JAVA_HOME:\u0026#34;, viper.Get(\u0026#34;java_home\u0026#34;)) } 其他环境变量也是一样的，上面输出：\n1 2 3 $ go run main.go GOPATH: D:\\GoSettings\\GoPath JAVA_HOME: E:\\Java\\jdk-17.0.5 也可以单独绑定环境变量：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func init() { // 绑定环境变量 _ = viper.BindEnv(\u0026#34;redis.port\u0026#34;, \u0026#34;redis_port\u0026#34;) _ = viper.BindEnv(\u0026#34;username\u0026#34;) } func main() { fmt.Println(\u0026#34;redis port:\u0026#34;, viper.Get(\u0026#34;redis.port\u0026#34;)) fmt.Println(\u0026#34;username:\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) } 调用BindEnv方法，如果只传入一个参数，则这个参数既表示键名，又表示环境变量名。 如果传入两个参数，则第一个参数表示键名，第二个参数表示环境变量名。\n上面将运行将输出：\n1 2 3 $ username=arlettebrook REDIS_PORT=10809 go run main.go redis port: 10809 username: arlettebrook 如果对应的环境变量不存在，viper 会自动将键名全部大小写转换再查找一次。所以，使用键名REDIS_PORT也能读取环境变量redis.port的值。\n另外，嵌套的配置键，绑定环境变量时必须指定环境变量名，因为 Viper 不会自动将点转换为下划线或其他分隔符。\n但可以设置环境变量名的替换符，就可以不用知道第二个参数\n1 2 // 将键名中的 . 或 - 替换成 _ viper.SetEnvKeyReplacer(strings.NewReplacer(\u0026#34;.\u0026#34;, \u0026#34;_\u0026#34;, \u0026#34;-\u0026#34;, \u0026#34;_\u0026#34;)) 完整代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func init() { // 绑定环境变量 _ = viper.BindEnv(\u0026#34;redis.port\u0026#34;) _ = viper.BindEnv(\u0026#34;username-a\u0026#34;) viper.SetEnvKeyReplacer(strings.NewReplacer(\u0026#34;.\u0026#34;, \u0026#34;_\u0026#34;, \u0026#34;-\u0026#34;, \u0026#34;_\u0026#34;)) } func main() { fmt.Println(\u0026#34;redis port:\u0026#34;, viper.Get(\u0026#34;redis.port\u0026#34;)) fmt.Println(\u0026#34;username-a:\u0026#34;, viper.Get(\u0026#34;username-a\u0026#34;)) } 演示输出：\n1 2 3 $ username_A=arlettebrook REDIS_PORT=10809 go run main.go redis port: 10809 username-a: arlettebrook 从命令行参数读取配置 Viper 支持 pflag 包（它们其实都在 spf13 仓库下），能够绑定命令行标志，从而读取命令行参数。\n同 BindEnv 类似，在调用绑定方法时，不会设置值，而是在每次访问时设置。这意味着我们可以随时绑定它，例如可以在 init() 函数中。\nBindPFlag：对于单个标志，可以调用此方法进行绑定。 BindPFlags：可以绑定一组现有的标志集 pflag.FlagSet。 示例程序如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/pflag\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) var ( _ = pflag.StringP(\u0026#34;username\u0026#34;, \u0026#34;u\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;help message for username\u0026#34;) _ = pflag.StringP(\u0026#34;password\u0026#34;, \u0026#34;p\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;help message for password\u0026#34;) ) func main() { pflag.Parse() _ = viper.BindPFlag(\u0026#34;username\u0026#34;, pflag.Lookup(\u0026#34;username\u0026#34;)) // 绑定单个标志 _ = viper.BindPFlags(pflag.CommandLine) // 绑定标志集 // 读取配置值 fmt.Printf(\u0026#34;username: %s\\n\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) fmt.Printf(\u0026#34;password: %s\\n\u0026#34;, viper.Get(\u0026#34;password\u0026#34;)) } 执行以上示例代码得到如下输出：\n1 2 3 $ go run main.go -u arlettebrook -p 123456 username: arlettebrook password: 123456 因为 pflag 能够兼容标准库的 flag 包，所以我们也可以变相的让 Viper 支持 flag。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package main import ( \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/pflag\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { _ = flag.String(\u0026#34;username\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;help message for username\u0026#34;) pflag.CommandLine.AddGoFlagSet(flag.CommandLine) // 将 flag 命令行参数注册到 pflag pflag.Parse() _ = viper.BindPFlags(pflag.CommandLine) // 读取配置值 fmt.Printf(\u0026#34;username: %s\\n\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) } 执行以上示例代码得到如下输出：\n1 2 $ go run main.go --username arlettebrook username: arlettebrook 如果你不使用 flag 或 pflag，则 Viper 还提供了 Go 接口的形式来支持其他 Flags，具体用法可以参考官方文档。\n命令行选项 如果一个键没有通过viper.Set显示设置值，那么获取时将尝试从命令行选项中读取。 如果有，优先使用。viper 使用 pflag 库来解析选项。 我们首先在init方法中定义选项，并且调用viper.BindPFlags绑定选项到配置中：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/pflag\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func init() { pflag.Int(\u0026#34;redis.port\u0026#34;, 8381, \u0026#34;Redis port to connect\u0026#34;) // 绑定命令行 _ = viper.BindPFlags(pflag.CommandLine) } func main() { pflag.Parse() viper.SetConfigFile(\u0026#34;./config.toml\u0026#34;) _ = viper.ReadInConfig() fmt.Println(viper.Get(\u0026#34;app_name\u0026#34;)) fmt.Println(viper.Get(\u0026#34;log_level\u0026#34;)) fmt.Println(\u0026#34;mysql ip: \u0026#34;, viper.Get(\u0026#34;mysql.ip\u0026#34;)) fmt.Println(\u0026#34;mysql port: \u0026#34;, viper.Get(\u0026#34;mysql.port\u0026#34;)) fmt.Println(\u0026#34;mysql user: \u0026#34;, viper.Get(\u0026#34;mysql.user\u0026#34;)) fmt.Println(\u0026#34;mysql password: \u0026#34;, viper.Get(\u0026#34;mysql.password\u0026#34;)) fmt.Println(\u0026#34;mysql database: \u0026#34;, viper.Get(\u0026#34;mysql.database\u0026#34;)) fmt.Println(\u0026#34;redis ip: \u0026#34;, viper.Get(\u0026#34;redis.ip\u0026#34;)) fmt.Println(\u0026#34;redis port: \u0026#34;, viper.Get(\u0026#34;redis.port\u0026#34;)) } 编译、运行程序：\n1 2 3 4 5 6 7 8 9 10 $ go run main.go --redis.port 9381 awesome web DEBUG mysql ip: 127.0.0.1 mysql port: 3306 mysql user: dj mysql password: 123456 mysql database: awesome redis ip: 127.0.0.1 redis port: 9381 如何不传入选项：\n将使用环境变量的配置，没有，在使用配置文件的配置，没有，在使用默认，都没有，为对应类型的零值。\n将使用配置文件的配置redis port: 7381，如果配置文件没有配置，才会使用默认值。这里没有默认值。\n从远程 key/value 存储读取配置 要在 Viper 中启用远程支持，需要匿名导入 viper/remote 包：\n1 import _ \u0026#34;github.com/spf13/viper/remote\u0026#34; Viper 支持 etcd、Consul 等远程 key/value 存储，这里以 Consul 为例进行讲解。\n首先需要准备 Consul 环境，最方便快捷的方式就是启动一个 Docker 容器：\n1 2 3 4 5 6 $ docker run \\ -d \\ -p 8500:8500 \\ -p 8600:8600/udp \\ --name=badger \\ consul agent -server -ui -node=server-1 -bootstrap-expect=1 -client=0.0.0.0 Docker 容器启动好后，浏览器访问 http://localhost:8500/，即可进入 Consul 控制台，在 user/config 路径下编写 YAML 格式的配置。\n使用 Viper 从 Consul 读取配置示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; _ \u0026#34;github.com/spf13/viper/remote\u0026#34; // 必须导入，才能加载远程 key/value 配置 ) func main() { viper.AddRemoteProvider(\u0026#34;consul\u0026#34;, \u0026#34;localhost:8500\u0026#34;, \u0026#34;user/config\u0026#34;) // 连接远程 consul 服务 viper.SetConfigType(\u0026#34;YAML\u0026#34;) // 显式设置文件格式文 YAML viper.ReadRemoteConfig() // 读取配置值 fmt.Printf(\u0026#34;username: %s\\n\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) fmt.Printf(\u0026#34;server.ip: %s\\n\u0026#34;, viper.Get(\u0026#34;server.ip\u0026#34;)) } 执行以上示例代码得到如下输出：\n1 2 3 $ go run main.go username: jianghushinian server.ip: 127.0.0.1 笔记：如果你想停止通过 Docker 安装的 Consul 容器，则可以执行 docker stop badger 命令。如果需要删除，则可以执行 docker rm badger 命令。\n从 Viper 中读取配置值 前文中我们介绍了各种将配置读入 Viper 的技巧，现在该学习如何使用这些配置了。\n在 Viper 中，有如下几种方法可以获取配置值：\nGet(key string) interface{}：获取配置项 key 所对应的值，key 不区分大小写，返回接口类型。 Get\u0026lt;Type\u0026gt;(key string) \u0026lt;Type\u0026gt;：获取指定类型的配置值， 可以是 Viper 支持的类型：GetBool、GetFloat64、GetInt、GetIntSlice、GetString、GetStringMap、GetStringMapString、GetStringSlice、GetTime、GetDuration。 AllSettings() map[string]interface{}：返回所有配置。根据我的经验，如果使用环境变量指定配置，则只能获取到通过 BindEnv 绑定的环境变量，无法获取到通过 AutomaticEnv 绑定的环境变量。 IsSet(key string) bool：值得注意的是，在使用 Get 或 Get\u0026lt;Type\u0026gt; 获取配置值，如果找不到，则每个 Get 函数都会返回一个零值。为了检查给定的键是否存在，可以使用 IsSet 方法，存在返回 true，不存在返回 false。 访问嵌套的键 有如下配置文件 config.yaml：\n1 2 3 4 5 username: arlettebrook password: 123456 server: ip: 127.0.0.1 port: 8080 可以通过 . 分隔符来访问嵌套字段。\n1 viper.Get(\u0026#34;server.ip\u0026#34;) 示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetConfigFile(\u0026#34;./config.yaml\u0026#34;) err := viper.ReadInConfig() if err != nil { log.Fatalln(\u0026#34;加载配置文件失败：\u0026#34;, err) } // 读取配置值 fmt.Printf(\u0026#34;username: %v\\n\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) fmt.Printf(\u0026#34;server: %v\\n\u0026#34;, viper.Get(\u0026#34;server\u0026#34;)) fmt.Printf(\u0026#34;server.ip: %v\\n\u0026#34;, viper.Get(\u0026#34;server.ip\u0026#34;)) fmt.Printf(\u0026#34;server.port: %v\\n\u0026#34;, viper.Get(\u0026#34;server.port\u0026#34;)) } 执行以上示例代码得到如下输出：\n1 2 3 4 5 $ go run main.go username: arlettebrook server: map[ip:127.0.0.1 port:8080] server.ip: 127.0.0.1 server.port: 8080 有一种情况是，配置中本就存在着叫 server.ip 的键，那么它会遮蔽 server 对象下的 ip 配置项。\n1 2 3 4 5 6 username: arlettebrook password: 123456 server: ip: 127.0.0.1 port: 8080 server.ip: 10.0.0.1 示例程序如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetConfigFile(\u0026#34;./config.yaml\u0026#34;) err := viper.ReadInConfig() if err != nil { log.Println(\u0026#34;加载配置文件出错：\u0026#34;, err) } // 读取配置值 fmt.Printf(\u0026#34;username: %v\\n\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) fmt.Printf(\u0026#34;server: %v\\n\u0026#34;, viper.Get(\u0026#34;server\u0026#34;)) fmt.Printf(\u0026#34;server.ip: %v\\n\u0026#34;, viper.Get(\u0026#34;server.ip\u0026#34;)) fmt.Printf(\u0026#34;server.port: %v\\n\u0026#34;, viper.Get(\u0026#34;server.port\u0026#34;)) } 执行以上示例代码得到如下输出：\n1 2 3 4 5 $ go run main.go username: arlettebrook server: map[ip:127.0.0.1 port:8080] server.ip: 10.0.0.1 server.port: 8080 server.ip 打印结果为 10.0.0.1，而不再是 server map 中所对应的值 127.0.0.1。\n提取子树 当使用 Viper 读取 config.yaml 配置文件后，viper 对象就包含了所有配置，并能通过 viper.Get(\u0026quot;server.ip\u0026quot;) 获取子配置。\n我们可以将这份配置理解为一颗树形结构，viper 对象就包含了这个完整的树，可以使用如下方法获取 server 子树。\n1 srvCfg := viper.Sub(\u0026#34;server\u0026#34;) 使用示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetConfigFile(\u0026#34;./config.yaml\u0026#34;) _ = viper.ReadInConfig() // 获取 server 子树 srvCfg := viper.Sub(\u0026#34;server\u0026#34;) // 读取配置值 fmt.Printf(\u0026#34;ip: %v\\n\u0026#34;, srvCfg.Get(\u0026#34;ip\u0026#34;)) fmt.Printf(\u0026#34;port: %v\\n\u0026#34;, srvCfg.Get(\u0026#34;port\u0026#34;)) fmt.Printf(\u0026#34;server.ip: %v\\n\u0026#34;, viper.Get(\u0026#34;server.ip\u0026#34;)) } 执行以上示例代码得到如下输出：\n1 2 3 4 $ go run main.go ip: 127.0.0.1 port: 8080 server.ip: 10.0.0.1 这里键没有出现覆盖的情况\n反序列化 Viper 提供了 2 个方法进行反序列化操作，以此来实现将所有或特定的值解析到结构体、map 等。\nUnmarshal(rawVal interface{}) : error：反序列化所有配置项。 UnmarshalKey(key string, rawVal interface{}) : error：反序列化指定配置项。 使用示例如下：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) type Config struct { Username string Password string // Viper 支持嵌套结构体 Server struct { IP string Port int } } func main() { viper.SetConfigFile(\u0026#34;./config.yaml\u0026#34;) _ = viper.ReadInConfig() var cfg Config if err := viper.Unmarshal(\u0026amp;cfg); err != nil { log.Fatalln(\u0026#34;反序列化错误：\u0026#34;, err) } var Password string if err := viper.UnmarshalKey(\u0026#34;Password\u0026#34;, \u0026amp;Password); err != nil { log.Fatalln(\u0026#34;反序列化错误：\u0026#34;, err) } fmt.Printf(\u0026#34;cfg: %+v\\n\u0026#34;, cfg) fmt.Printf(\u0026#34;Password: %s\\n\u0026#34;, Password) } 执行以上示例代码得到如下输出：\n1 2 3 $ go run main.go cfg: {Username:arlettebrook Password:123456 Server:{IP:10.0.0.1 Port:8080}} Password: 123456 如果配置项的 key 本身就包含 .，则需要修改分隔符。\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) type Config struct { Chart struct { Values map[string]interface{} } } func main() { // 默认的键分隔符为 `.`，这里将其修改为 `::` v := viper.NewWithOptions(viper.KeyDelimiter(\u0026#34;::\u0026#34;)) v.SetDefault(\u0026#34;chart::values\u0026#34;, map[string]interface{}{ \u0026#34;ingress\u0026#34;: map[string]interface{}{ \u0026#34;annotations\u0026#34;: map[string]interface{}{ \u0026#34;traefik.frontend.rule.type\u0026#34;: \u0026#34;PathPrefix\u0026#34;, \u0026#34;traefik.ingress.kubernetes.io/ssl-redirect\u0026#34;: \u0026#34;true\u0026#34;, }, }, }) var cfg Config if err := v.Unmarshal(\u0026amp;cfg); err != nil { panic(err) } fmt.Printf(\u0026#34;cfg: %+v\\n\u0026#34;, cfg) } 执行以上示例代码得到如下输出：\n1 2 3 $ go run main.go cfg: {Chart:{Values:map[ingress:map[annotations:map[traefik.frontend.rule.type:PathPrefix traefik. ingress.kubernetes.io/ssl-redirect:true]]]}} 注意⚠️：Viper 在后台使用 mapstructure 来解析值，其默认情况下使用 mapstructure tags。当我们需要将 Viper 读取的配置反序列到结构体中时，如果出现结构体字段跟配置项不匹配，则可以设置 mapstructure tags 来解决。\nUnmarshal viper 支持将配置Unmarshal到一个结构体中，为结构体中的对应字段赋值。\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 45 46 47 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) type Config struct { AppName string LogLevel string MySQL MySQLConfig Redis RedisConfig } type MySQLConfig struct { IP string Port int User string Password string Database string } type RedisConfig struct { IP string Port int } func main() { viper.SetConfigName(\u0026#34;config\u0026#34;) viper.SetConfigType(\u0026#34;toml\u0026#34;) viper.AddConfigPath(\u0026#34;.\u0026#34;) err := viper.ReadInConfig() if err != nil { log.Fatalf(\u0026#34;read config failed: %v\u0026#34;, err) } var c Config err = viper.Unmarshal(\u0026amp;c) if err != nil { log.Fatalf(\u0026#34;反序列化失败：%v\u0026#34;, err) } fmt.Println(c.MySQL) } 编译，运行程序，输出：\n1 2 $ go run main.go {127.0.0.1 3306 dj 123456 awesome} 序列化 一个好用的配置包不仅能够支持反序列化操作，还要支持序列化操作。Viper 支持将配置序列化成字符串，或直接序列化到文件中。\n序列化成字符串 我们可以将全部配置序列化配置为 YAML 格式字符串。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; \u0026#34;gopkg.in/yaml.v3\u0026#34; ) // 序列化配置为 YAML 格式字符串 func yamlStringSettings() string { c := viper.AllSettings() // 获取全部配置 bs, _ := yaml.Marshal(c) // 根据需求序列化成不同格式 return string(bs) } func main() { viper.SetConfigFile(\u0026#34;./config.yaml\u0026#34;) _ = viper.ReadInConfig() fmt.Printf(yamlStringSettings()) } 执行以上示例代码得到如下输出：\n1 2 3 4 5 6 $ go run main.go password: 123456 server: ip: 10.0.0.1 port: 8080 username: arlettebrook 写入配置文件 Viper 还支持直接将配置序列化到文件中，提供了如下几个方法：\nWriteConfig：将当前的 viper 配置写入预定义路径。如果没有预定义路径，则会报错。如果预定义路径已经存在配置文件，将会被覆盖。 SafeWriteConfig：将当前的 viper 配置写入预定义路径。如果没有预定义路径，则会报错。如果预定义路径已经存在配置文件，不会覆盖，会报错。 WriteConfigAs： 将当前的 viper 配置写入给定的文件路径。如果给定的文件路径已经存在配置文件，将会被覆盖。 SafeWriteConfigAs：将当前的 viper 配置写入给定的文件路径。如果给定的文件路径已经存在配置文件，不会覆盖，会报错。 注意保存的文件类型要与配置类型一直，否则会报错config type could not be determined for XXX。 使用示例：\n1 2 3 4 5 6 viper.WriteConfig() // 将当前配置写入由 `viper.AddConfigPath()` 和 `viper.SetConfigFile()` 设置的预定义路径。类型就为配置类型。 viper.SafeWriteConfig() // 将会报错，因为它已经被写入了。 viper.WriteConfigAs(\u0026#34;./cfg.yaml\u0026#34;) // 文件类型要与配置类型一直，否则报错 viper.SafeWriteConfigAs(\u0026#34;./cfg.yaml\u0026#34;) // 将会报错，因为它已经被写入了。 viper.SafeWriteConfigAs(\u0026#34;./cfg/cfg.yaml\u0026#34;) 1 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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; \u0026#34;gopkg.in/yaml.v3\u0026#34; ) // 序列化配置为 YAML 格式字符串 func yamlStringSettings() string { c := viper.AllSettings() // 获取全部配置 bs, _ := yaml.Marshal(c) // 根据需求序列化成不同格式 return string(bs) } func main() { viper.SetConfigFile(\u0026#34;./config.yaml\u0026#34;) _ = viper.ReadInConfig() fmt.Printf(yamlStringSettings()) viper.Set(\u0026#34;username\u0026#34;, \u0026#34;哈哈哈\u0026#34;) err := viper.WriteConfigAs(\u0026#34;./cfg.yaml\u0026#34;) if err != nil { panic(err) } fmt.Println(\u0026#34;修改后的username:\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) } 输出：\n1 2 3 4 5 6 7 $ go run main.go password: 123456 server: ip: 10.0.0.1 port: 8080 username: arlettebrook 修改后的username: 哈哈哈 保存配置 有时候，我们想要将程序中生成的配置，或者所做的修改保存下来。viper 提供了接口！\nWriteConfig：将当前的 viper 配置写到预定义路径，如果没有预定义路径，返回错误。将会覆盖当前配置； SafeWriteConfig：与上面功能一样，但是如果配置文件存在，则不覆盖； WriteConfigAs：保存配置到指定路径，如果文件存在，则覆盖； SafeWriteConfig：与上面功能一样，但是入股配置文件存在，则不覆盖。 下面我们通过程序生成一个config.toml配置：\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 package main import ( \u0026#34;log\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetConfigName(\u0026#34;config\u0026#34;) viper.SetConfigType(\u0026#34;toml\u0026#34;) viper.AddConfigPath(\u0026#34;.\u0026#34;) viper.Set(\u0026#34;app_name\u0026#34;, \u0026#34;awesome web\u0026#34;) viper.Set(\u0026#34;log_level\u0026#34;, \u0026#34;DEBUG\u0026#34;) viper.Set(\u0026#34;mysql.ip\u0026#34;, \u0026#34;127.0.0.1\u0026#34;) viper.Set(\u0026#34;mysql.port\u0026#34;, 3306) viper.Set(\u0026#34;mysql.user\u0026#34;, \u0026#34;root\u0026#34;) viper.Set(\u0026#34;mysql.password\u0026#34;, \u0026#34;123456\u0026#34;) viper.Set(\u0026#34;mysql.database\u0026#34;, \u0026#34;awesome\u0026#34;) viper.Set(\u0026#34;redis.ip\u0026#34;, \u0026#34;127.0.0.1\u0026#34;) viper.Set(\u0026#34;redis.port\u0026#34;, 6381) err := viper.SafeWriteConfig() if err != nil { log.Fatal(\u0026#34;write config failed: \u0026#34;, err) } } 多实例对象 由于大多数应用程序都希望使用单个配置实例对象来管理配置，因此 viper 包默认提供了这一功能，它类似于一个单例。当我们使用 Viper 时不需要配置或初始化，Viper 实现了开箱即用的效果。\n在上面的所有示例中，演示了如何以单例方式使用 Viper。我们还可以创建多个不同的 Viper 实例以供应用程序中使用，每个实例都有自己单独的一组配置和值，并且它们可以从不同的配置文件、key/value 存储等位置读取配置信息。\nViper 包支持的所有功能都被镜像为 viper 对象上的方法，这种设计思路在 Go 语言中非常常见，如标准库中的 log 包。\n多实例使用示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { x := viper.New() y := viper.New() x.SetConfigFile(\u0026#34;./config.yaml\u0026#34;) _ = x.ReadInConfig() fmt.Printf(\u0026#34;x.username: %v\\n\u0026#34;, x.Get(\u0026#34;username\u0026#34;)) y.SetDefault(\u0026#34;username\u0026#34;, \u0026#34;多实例对象\u0026#34;) fmt.Printf(\u0026#34;y.username: %v\\n\u0026#34;, y.Get(\u0026#34;username\u0026#34;)) viper.SetDefault(\u0026#34;username\u0026#34;, \u0026#34;默认单实例对象\u0026#34;) fmt.Printf(\u0026#34;viper.username: %v\\n\u0026#34;, viper.Get(\u0026#34;username\u0026#34;)) } 在这里，我创建了两个 Viper 实例 x 和 y，它们分别从配置文件读取配置和通过默认值的方式设置配置，使用时互不影响，使用者可以自行管理它们的生命周期。\n执行以上示例代码得到如下输出：\n1 2 3 4 $ go run main.go x.username: arlettebrook y.username: 多实例对象 viper.username: 默认单实例对象 使用建议 Viper 提供了众多方法可以管理配置，在实际项目开发中我们可以根据需要进行使用。如果是小型项目，推荐直接使用 viper 实例管理配置。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetConfigFile(\u0026#34;./config.yaml\u0026#34;) if err := viper.ReadInConfig(); err != nil { panic(fmt.Errorf(\u0026#34;read config file error: %s \\n\u0026#34;, err.Error())) } // 监控配置文件变化 viper.WatchConfig() // use config... fmt.Println(viper.Get(\u0026#34;username\u0026#34;)) } 如果是中大型项目，一般都会有一个用来记录配置的结构体，可以使用 Viper 将配置反序列化到结构体中。\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 45 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/fsnotify/fsnotify\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) type Config struct { Username string Password string // Viper 支持嵌套结构体 Server struct { IP string Port int } } func main() { viper.SetConfigFile(\u0026#34;./config.yaml\u0026#34;) if err := viper.ReadInConfig(); err != nil { panic(fmt.Errorf(\u0026#34;read config file error: %s \\n\u0026#34;, err.Error())) } // 将配置信息反序列化到结构体中 var cfg Config if err := viper.Unmarshal(\u0026amp;cfg); err != nil { panic(fmt.Errorf(\u0026#34;unmarshal config error: %s \\n\u0026#34;, err.Error())) } // 注册每次配置文件发生变更后都会调用的回调函数 viper.OnConfigChange(func(e fsnotify.Event) { // 每次配置文件发生变化，需要重新将其反序列化到结构体中 if err := viper.Unmarshal(\u0026amp;cfg); err != nil { panic(fmt.Errorf(\u0026#34;unmarshal config error: %s \\n\u0026#34;, err.Error())) } }) // 监控配置文件变化 viper.WatchConfig() // use config... fmt.Println(cfg.Username) } 需要注意的是，直接使用 viper 实例管理配置的情况下，当我们通过 viper.WatchConfig() 监听了配置文件变化，如果配置变化，则变化会立刻体现在 viper 实例对象上，下次通过 viper.Get() 获取的配置即为最新配置。但是在使用结构体管理配置时，viper 实例对象变化了，记录配置的结构体 Config 是不会自动更新的，所以需要使用 viper.OnConfigChange 在回调函数中重新将变更后的配置反序列化到 Config 中。\n总结 本文探讨 Viper 的各种用法和使用场景，首先说明了为什么使用 Viper，它的优势是什么。\n接着讲解了 Viper 包中最核心的两个功能：如何把配置值读入 Viper 和从 Viper 中读取配置值。Viper 对着两个功能都提供了非常多的方法来支持。\n然后又介绍了如何用 Viper 来管理多份配置，即使用多实例。\n对于 Viper 的使用我也给出了自己的建议，针对小型项目，推荐直接使用 viper 实例管理配置，如果是中大型项目，则推荐使用结构体来管理配置。\n最后，Viper 正在向着 v2 版本迈进，欢迎读者在这里分享想法，也期待下次来写一篇 v2 版本的文章与读者一起学习进步。\n参考 Viper 源码仓库： https://github.com/spf13/viper 搬运：在 Go 中如何使用 Viper 来管理配置 Go 每日一库之 viper ","date":"2024-05-03T18:24:28+08:00","permalink":"https://arlettebrook.github.io/p/go%E9%85%8D%E7%BD%AE%E7%AE%A1%E7%90%86%E4%B9%8B%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93viper/","title":"Go配置管理之第三方库viper"},{"content":" 介绍 Cobra是一个命令行程序库，可以用来编写命令行程序。同时，它也提供了一个脚手架， 用于生成基于 cobra 的应用程序框架。非常多知名的开源项目使用了 cobra 库构建命令行，如Kubernetes、Hugo、etcd等等等等。 本文介绍 cobra 库的基本使用和一些有趣的特性。\n关于作者spf13，这里多说两句。spf13 开源不少项目，而且他的开源项目质量都比较高。 相信使用过 vim 的都知道spf13-vim，号称 vim 终极配置。 可以一键配置，对于我这样的懒人来说绝对是福音。他的viper是一个完整的配置解决方案。 完美支持 JSON/TOML/YAML/HCL/envfile/Java properties 配置文件等格式，还有一些比较实用的特性，如配置热更新、多查找目录、配置保存等。 还有非常火的静态网站生成器hugo也是他的作品。\nCobra 是一个 Go 语言开发的命令行（CLI）框架，它提供了简洁、灵活且强大的方式来创建命令行程序。它包含一个用于创建命令行程序的库（Cobra 库），以及一个用于快速生成基于 Cobra 库的命令行程序工具（Cobra 命令）。Cobra 是由 Go 团队成员 spf13 为 Hugo 项目创建的，并已被许多流行的 Go 项目所采用，如 Kubernetes、Helm、Docker (distribution)、Etcd 等。\n概念 Cobra 建立在命令、参数和标志这三个结构之上。要使用 Cobra 编写一个命令行程序，需要明确这三个概念。\n命令（COMMAND）：命令表示要执行的操作。 参数（ARG）：是命令的参数，一般用来表示操作的对象。 标志（FLAG）：是命令的修饰，可以调整操作的行为。 一个好的命令行程序在使用时读起来像句子，用户会自然的理解并知道如何使用该程序。\n要编写一个好的命令行程序，需要遵循的模式是 APPNAME VERB NOUN --ADJECTIVE 或 APPNAME COMMAND ARG --FLAG。\n在这里 VERB 代表动词，NOUN 代表名词，ADJECTIVE 代表形容词。\n以下是一个现实世界中好的命令行程序的例子：\n1 $ hugo server --port=1313 以上示例中，server 是一个命令（子命令），port 是一个标志（1313 是标志的参数，但不是命令的参数 ARG）。\n下面是一个 git 命令的例子：\n1 $ git clone URL --bare 以上示例中，clone 是一个命令（子命令），URL 是命令的参数，bare 是标志。\n特性 cobra 提供非常丰富的功能：\n轻松支持子命令，如app server，app fetch等； 完全兼容 POSIX 选项（包括短、长选项）； 嵌套子命令； 全局、本地层级选项。可以在多处设置选项，按照一定的顺序取用； 使用脚手架轻松生成程序框架和命令。 等。 快速使用 要使用 Cobra 创建命令行程序，需要先通过如下命令进行下载并添加到项目：\n1 go get -u github.com/spf13/cobra@latest 安装好后，就可以像其他 Go 语言库一样导入 Cobra 包并使用了。\n1 import \u0026#34;github.com/spf13/cobra\u0026#34; 创建一个命令 假设我们要创建的命令行程序叫作 hugo，可以编写如下代码创建一个命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // hugo/cmd/root.go var rootCmd = \u0026amp;cobra.Command{ Use: \u0026#34;hugo\u0026#34;, Short: \u0026#34;Hugo is a very fast static site generator\u0026#34;, Long: `A Fast and Flexible Static Site Generator built with love by spf13 and friends in Go. Complete documentation is available at https://gohugo.io`, Run: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;run hugo...\u0026#34;) }, } func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } cobra.Command 是一个结构体，代表一个命令，其各个属性含义如下：\nUse 是命令的名称。指定使用信息，即命令怎么被调用，格式为name arg1 [arg2]。name为命令名，后面的arg1为必填参数，arg3为可选参数，参数可以多个。参数一般用在子命令上。\nShort 代表当前命令的简短描述。\nLong 表示当前命令的完整描述。\nRun 属性是一个函数，当执行命令时会调用此函数。\nrootCmd.Execute() 是命令的执行入口，其内部会解析 os.Args[1:] 参数列表（默认情况下是这样，也可以通过 Command.SetArgs 方法设置参数），然后遍历命令树，为命令找到合适的匹配项和对应的标志。\n创建 main.go 按照编写 Go 程序的惯例，我们要为 hugo 程序编写一个 main.go 文件，作为程序的启动入口。\n1 2 3 4 5 6 7 8 9 10 // hugo/main.go package main import ( \u0026#34;hugo/cmd\u0026#34; ) func main() { cmd.Execute() } main.go 代码实现非常简单，只在 main 函数中调用了 cmd.Execute() 函数，来执行命令。\n编译并运行命令 现在，我们就可以编译并运行这个命令行程序了。\n1 2 3 4 5 # 编译 $ go build -o hugo # 执行 $ ./hugo run hugo... 笔记：示例代码里没有打印 Run 函数的 args 参数内容，你可以自行打印看看结果（提示：args 为命令行参数列表）。\n以上我们编译并执行了 hugo 程序，输出内容正是 cobra.Command 结构体中 Run 函数内部代码的执行结果。\n我们还可以使用 --help 查看这个命令行程序的使用帮助。\n1 2 3 4 5 6 7 8 9 10 $ ./hugo --help A Fast and Flexible Static Site Generator built with love by spf13 and friends in Go. Complete documentation is available at https://gohugo.io Usage: hugo [flags] Flags: -h, --help help for hugo 这里打印了 cobra.Command 结构体中 Long 属性的内容，如果 Long 属性不存在，则打印 Short 属性内容。\nhugo 命令用法为 hugo [flags]，如 hugo --help。\n这个命令行程序自动支持了 -h/--help 标志。\n以上就是使用 Cobra 编写一个命令行程序最常见的套路，这也是 Cobra 推荐写法。\n当前项目目录结构如下：\n1 2 3 4 5 6 7 $ tree hugo hugo ├── cmd │ └── root.go ├── go.mod ├── go.sum └── main.go Cobra 程序目录结构基本如此，main.go 作为命令行程序的入口，不要写过多的业务逻辑，所有命令都应该放在 cmd/ 目录下，以后不管编写多么复杂的命令行程序都可以这么来设计。\n在 cobra 中，命令和子命令都是用Command结构表示的。Command有非常多的字段，用来定制命令的行为。 在实际中，最常用的就那么几个。我们在前面示例中已经看到了Use/Short/Long/Run。\nUse指定使用信息，即命令怎么被调用，格式为name arg1 [arg2]。name为命令名，后面的arg1为必填参数，arg3为可选参数，参数可以多个。\nShort/Long都是指定命令的帮助信息，只是前者简短，后者详尽而已。\nRun是实际执行操作的函数。\n定义新的子命令很简单，就是创建一个cobra.Command变量，设置一些字段，然后添加到根命令中\n添加子命令 与定义 rootCmd 一样，我们可以使用 cobra.Command 定义其他命令，并通过 rootCmd.AddCommand() 方法将其添加为 rootCmd 的一个子命令。\n1 2 3 4 5 6 7 8 9 10 11 12 var versionCmd = \u0026amp;cobra.Command{ Use: \u0026#34;version\u0026#34;, Short: \u0026#34;Print the version number of Hugo\u0026#34;, Long: `All software has versions. This is Hugo\u0026#39;s`, Run: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;Hugo Static Site Generator v0.9 -- HEAD\u0026#34;) }, } func init() { rootCmd.AddCommand(versionCmd) } 现在重新编译并运行命令行程序。\n1 2 3 $ go build -o hugo $ ./hugo version Hugo Static Site Generator v0.9 -- HEAD 可以发现 version 命令已经被加入进来了。\n再次查看帮助信息：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 $ ./hugo -h A Fast and Flexible Static Site Generator built with love by spf13 and friends in Go. Complete documentation is available at https://gohugo.io Usage: hugo [flags] hugo [command] Available Commands: completion Generate the autocompletion script for the specified shell help Help about any command version Print the version number of Hugo Flags: -h, --help help for hugo Use \u0026#34;hugo [command] --help\u0026#34; for more information about a command. 这次的帮助信息更为丰富，除了可以使用 hugo [flags] 语法，由于子命令的加入，又多了一个 hugo [command] 语法可以使用，如 hugo version。\n现在有三个可用命令：\ncompletion 可以为指定的 Shell 生成自动补全脚本，将在 Shell 补全 小节进行讲解。\nhelp 用来查看帮助，同 -h/--help 类似，可以使用 hugo help command 语法查看 command 命令的帮助信息。\nversion 为新添加的子命令。\n查看子命令帮助信息：\n1 2 3 4 5 6 7 8 $ ./hugo help version All software has versions. This is Hugo\u0026#39;s Usage: hugo version [flags] Flags: -h, --help help for version 使用命令行标志 Cobra 完美适配 pflag，结合 pflag 可以更灵活的使用标志功能。\n提示：对 pflag 不熟悉的读者可以参考我的另一篇文章《Go解析命令行参数之第三方库pflag》。\n持久标志 如果一个标志是持久的，则意味着该标志将可用于它所分配的命令以及该命令下的所有子命令。\n对于全局标志，可以定义在根命令 rootCmd 上。\n1 2 3 4 5 // hugo/cmd/root.go func init() { var Verbose bool rootCmd.PersistentFlags().BoolVarP(\u0026amp;Verbose, \u0026#34;verbose\u0026#34;, \u0026#34;v\u0026#34;, false, \u0026#34;Verbose output\u0026#34;) } 本地标志 标志也可以是本地的，这意味着它只适用于该指定命令。\n1 2 3 4 5 // hugo/cmd/root.go func init() { var Source string rootCmd.Flags().StringVarP(\u0026amp;Source, \u0026#34;source\u0026#34;, \u0026#34;s\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;Source directory to read from\u0026#34;) } 父命令的本地标志 默认情况下，Cobra 仅解析目标命令上的本地标志，忽略父命令上的本地标志。通过在父命令上启用 Command.TraverseChildren 属性，Cobra 将在执行目标命令之前解析每个命令的本地标志。(在执行子命令之前解析所有父级上的标志。)\n1 2 3 4 var rootCmd = \u0026amp;cobra.Command{ Use: \u0026#34;hugo\u0026#34;, TraverseChildren: true, } 提示：如果你不理解，没关系，继续往下看，稍后会有示例代码演示讲解。\n必选标志 默认情况下，标志是可选的。我们可以将其标记为必选，如果运行目标命令时没有提供，则会报错。\n1 2 3 4 5 6 7 8 9 10 11 12 var Verbose bool var Source string var Region string // hugo/cmd/root.go func init() { rootCmd.PersistentFlags().BoolVarP(\u0026amp;Verbose, \u0026#34;verbose\u0026#34;, \u0026#34;v\u0026#34;, false, \u0026#34;Verbose output\u0026#34;) rootCmd.Flags().StringVarP(\u0026amp;Source, \u0026#34;source\u0026#34;, \u0026#34;s\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;Source directory to read from\u0026#34;) rootCmd.Flags().StringVarP(\u0026amp;Region, \u0026#34;region\u0026#34;, \u0026#34;r\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;AWS region (required)\u0026#34;) _ = rootCmd.MarkFlagRequired(\u0026#34;region\u0026#34;) } 定义好以上几个标志后，为了展示效果，我们对 rootCmd.Run 方法做些修改，分别打印 Verbose、Source、Region 几个变量。\n1 2 3 4 5 6 7 8 9 var rootCmd = \u0026amp;cobra.Command{ Use: \u0026#34;hugo\u0026#34;, Run: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;run hugo...\u0026#34;) fmt.Printf(\u0026#34;Verbose: %v\\n\u0026#34;, Verbose) fmt.Printf(\u0026#34;Source: %v\\n\u0026#34;, Source) fmt.Printf(\u0026#34;Region: %v\\n\u0026#34;, Region) }, } 另外，为了测试启用 Command.TraverseChildren 的效果，我又添加了一个 print 子命令。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var printCmd = \u0026amp;cobra.Command{ Use: \u0026#34;print [OPTIONS] [COMMANDS]\u0026#34;, Run: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;run print...\u0026#34;) fmt.Printf(\u0026#34;printFlag: %v\\n\u0026#34;, printFlag) fmt.Printf(\u0026#34;Source: %v\\n\u0026#34;, Source) }, } var printFlag string func init() { // 本地标志 printCmd.Flags().StringVarP(\u0026amp;printFlag, \u0026#34;flag\u0026#34;, \u0026#34;f\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;print flag for local\u0026#34;) rootCmd.AddCommand(printCmd) } 现在，我们重新编译并运行 hugo，来对上面添加的这几个标志进行测试。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ go build -o hugo $ ./hugo -h A Fast and Flexible Static Site Generator built with love by spf13 and friends in Go. Complete documentation is available at https://gohugo.io Usage: hugo [flags] hugo [command] Available Commands: completion Generate the autocompletion script for the specified shell help Help about any command print version Print the version number of Hugo Flags: -h, --help help for hugo -r, --region string AWS region (required) -s, --source string Source directory to read from -v, --verbose verbose output Use \u0026#34;hugo [command] --help\u0026#34; for more information about a command. 以上帮助信息清晰明了，我就不过多解释了。\n执行 hugo 命令：\n1 2 3 4 5 $ ./hugo -r test-region run hugo... Verbose: false Source: Region: test-region 现在 -r/--region 为必选标志，不传将会得到 Error: required flag(s) \u0026quot;region\u0026quot; not set 报错。\n执行 print 子命令：\n1 2 3 4 $ ./hugo print -f test-flag run print... printFlag: test-flag Source: 以上执行结果可以发现，父命令的标志 Source 内容为空。\n现在使用如下命令执行 print 子命令：\n1 2 3 4 $ ./hugo -s test-source print -f test-flag run print... printFlag: test-flag Source: test-source 在 print 子命令前，我们指定了 -s test-source 标志，-s/--source 是父命令 hugo 的标志，也能够被正确解析，这就是启用 Command.TraverseChildren 的效果。\n如果我们将 rootCmd 的 TraverseChildren 属性置为 false，则会得到 Error: unknown shorthand flag: 's' in -s 报错。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 指定 rootCmd.TraverseChildren = false 后，重新编译程序 $ go build -o hugo # 执行同样的命令，现在会得到报错 $ ./hugo -s test-source print -f test-flag Error: unknown shorthand flag: \u0026#39;s\u0026#39; in -s Usage: hugo print [OPTIONS] [COMMANDS] [flags] Flags: -f, --flag string print flag for local -h, --help help for print Global Flags: -v, --verbose verbose output unknown shorthand flag: \u0026#39;s\u0026#39; in -s 总结：在父命令上设置穿越儿童为true，运行子命令时会解析父命令的本地选项，反之，只解析子命令的本地选项以及持久选项。\n处理配置 除了将命令行标志的值绑定到变量，我们也可以将标志绑定到 Viper，这样就可以使用 viper.Get() 来获取标志的值了。\n1 2 3 4 5 6 var author string func init() { rootCmd.PersistentFlags().StringVar(\u0026amp;author, \u0026#34;author\u0026#34;, \u0026#34;YOUR NAME\u0026#34;, \u0026#34;Author name for copyright attribution\u0026#34;) viper.BindPFlag(\u0026#34;author\u0026#34;, rootCmd.PersistentFlags().Lookup(\u0026#34;author\u0026#34;)) } 提示：对 Viper 不熟悉的读者可以参考我的另一篇文章《Go配置管理之第三方库viper》。\n另外，我们可以使用 cobra.OnInitialize() 来初始化配置文件。\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 var cfgFile string func init() { cobra.OnInitialize(initConfig) rootCmd.Flags().StringVarP(\u0026amp;cfgFile, \u0026#34;config\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;config file\u0026#34;) } func initConfig() { if cfgFile != \u0026#34;\u0026#34; { viper.SetConfigFile(cfgFile) } else { home, err := homedir.Dir() if err != nil { fmt.Println(err) os.Exit(1) } viper.AddConfigPath(home) viper.SetConfigName(\u0026#34;.cobra_test\u0026#34;) } if err := viper.ReadInConfig(); err != nil { fmt.Println(\u0026#34;Can\u0026#39;t read config:\u0026#34;, err) os.Exit(1) } } 传递给 cobra.OnInitialize() 的函数 initConfig 函数将在调用命令的 Execute 方法时运行。\n为了展示使用 Cobra 处理配置的效果，需要修改 rootCmd.Run 函数的打印代码：\n1 2 3 4 5 6 7 8 9 10 11 var rootCmd = \u0026amp;cobra.Command{ Use: \u0026#34;hugo\u0026#34;, Run: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;run hugo...\u0026#34;) fmt.Printf(\u0026#34;Verbose: %v\\n\u0026#34;, Verbose) fmt.Printf(\u0026#34;Source: %v\\n\u0026#34;, Source) fmt.Printf(\u0026#34;Region: %v\\n\u0026#34;, Region) fmt.Printf(\u0026#34;Author: %v\\n\u0026#34;, viper.Get(\u0026#34;author\u0026#34;)) fmt.Printf(\u0026#34;Config: %v\\n\u0026#34;, viper.AllSettings()) }, } 提供 config.yaml 配置文件内容如下：\n1 2 3 4 5 password: 123456 server: ip: 10.0.0.1 port: 8080 username: arlettebrook 现在重新编译并运行 hugo 命令：\n1 2 3 4 5 6 7 8 $ go run main.go -r test-rergion run hugo... Verbose: false Source: Region: test-rergion Author: YOUR NAME Config: map[author:YOUR NAME password:123456 server:map[ip:10.0.0.1 port:8080] username:arlettebro ok] 注意事项：viper读取配置文件若根据文件名进行扫描，当文件名相同，后缀不同时，谁先扫到就用谁，在根据指定的类型进行解析。如果没有指定类型，解析的是什么类型，就是什么类型，但在最后保存时，需要指定具体的类型，不然保存不了，指定了类型就可以。\n笔记：Cobra 同时支持 pflag 和 Viper 两个库，实际上这三个库出自同一作者 spf13。\n参数验证 在执行命令行程序时，我们可能需要对命令参数进行合法性验证，cobra.Command 的 Args 属性提供了此功能。\nArgs 属性类型为一个函数：func(cmd *Command, args []string) error，可以用来验证参数。\nCobra 内置了以下验证函数：\nNoArgs：如果存在任何命令参数，该命令将报错。 ArbitraryArgs：该命令将接受任意参数。 OnlyValidArgs：如果有任何命令参数不在 Command 的 ValidArgs 字段中，该命令将报错。 MinimumNArgs(int)：如果没有至少 N 个命令参数，该命令将报错。 MaximumNArgs(int)：如果有超过 N 个命令参数，该命令将报错。 ExactArgs(int)：如果命令参数个数不为 N，该命令将报错。 ExactValidArgs(int)：如果命令参数个数不为 N，或者有任何命令参数不在 Command 的 ValidArgs 字段中，该命令将报错。 RangeArgs(min, max)：如果命令参数的数量不在预期的最小数量 min 和最大数量 max 之间，该命令将报错。 内置验证函数用法如下：\n1 2 3 4 5 6 7 var versionCmd = \u0026amp;cobra.Command{ Use: \u0026#34;version\u0026#34;, Run: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;Hugo Static Site Generator v0.9 -- HEAD\u0026#34;) }, Args: cobra.MaximumNArgs(2), // 使用内置的验证函数，位置参数多于 2 个则报错 } 重新编译并运行 hugo 命令：\n1 2 3 4 5 6 7 8 # 编译 $ go build -o hugo # 两个命令参数满足验证函数的要求 $ ./hugo version a b Hugo Static Site Generator v0.9 -- HEAD # 超过两个参数则报错 $ ./hugo version a b c Error: accepts at most 2 arg(s), received 3 当然，我们也可以自定义验证函数：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var printCmd = \u0026amp;cobra.Command{ Use: \u0026#34;print [OPTIONS] [COMMANDS]\u0026#34;, Run: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;run print...\u0026#34;) // 命令行位置参数列表：例如执行 `hugo print a b c d` 将得到 [a b c d] fmt.Printf(\u0026#34;args: %v\\n\u0026#34;, args) }, // 使用自定义验证函数 Args: func(cmd *cobra.Command, args []string) error { if len(args) \u0026lt; 1 { return errors.New(\u0026#34;requires at least one arg\u0026#34;) } if len(args) \u0026gt; 4 { return errors.New(\u0026#34;the number of args cannot exceed 4\u0026#34;) } if args[0] != \u0026#34;a\u0026#34; { return errors.New(\u0026#34;first argument must be \u0026#39;a\u0026#39;\u0026#34;) } return nil }, } 重新编译并运行 hugo 命令：\n1 2 3 4 5 6 7 8 9 10 11 12 # 编译 $ go build -o hugo # 1~4 个参数满足条件 $ ./hugo print a b c d run print... args: [a b c d] # 没有参数则报错 $ ./hugo print Error: requires at least one arg # 第一个参数不满足验证函数逻辑，也会报错 $ ./hugo print x Error: first argument must be \u0026#39;a\u0026#39; Hooks 在执行 Run 函数前后，我么可以执行一些钩子函数，其作用和执行顺序如下：\nPersistentPreRun：在 PreRun 函数执行之前执行，对此命令的子命令同样生效。 PreRun：在 Run 函数执行之前执行。 Run：执行命令时调用的函数，用来编写命令的业务逻辑。 PostRun：在 Run 函数执行之后执行。 PersistentPostRun：在 PostRun 函数执行之后执行，对此命令的子命令同样生效。 修改 rootCmd 如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var rootCmd = \u0026amp;cobra.Command{ Use: \u0026#34;hugo\u0026#34;, PersistentPreRun: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;hugo PersistentPreRun\u0026#34;) }, PreRun: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;hugo PreRun\u0026#34;) }, Run: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;run hugo...\u0026#34;) }, PostRun: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;hugo PostRun\u0026#34;) }, PersistentPostRun: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;hugo PersistentPostRun\u0026#34;) }, } 重新编译并运行 hugo 命令：\n1 2 3 4 5 6 7 8 9 # 编译 $ go build -o hugo # 执行 $ ./hugo hugo PersistentPreRun hugo PreRun run hugo... hugo PostRun hugo PersistentPostRun 输出顺序符合预期。\n其中 PersistentPreRun、PersistentPostRun 两个函数对子命令同样生效。\n1 2 3 4 $ ./hugo version hugo PersistentPreRun Hugo Static Site Generator v0.9 -- HEAD hugo PersistentPostRun 以上几个函数都有对应的 \u0026lt;Hooks\u0026gt;E 版本，E 表示 Error，即函数执行出错将会返回 Error，执行顺序不变：\nPersistentPreRunE PreRunE RunE PostRunE PersistentPostRunE 如果定义了 \u0026lt;Hooks\u0026gt;E 函数，则 \u0026lt;Hooks\u0026gt; 函数不会执行。比如同时定义了 Run 和 RunE，则只会执行 RunE，不会执行 Run，其他 Hooks 函数同理。\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 var rootCmd = \u0026amp;cobra.Command{ Use: \u0026#34;hugo\u0026#34;, PersistentPreRun: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;hugo PersistentPreRun\u0026#34;) }, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { fmt.Println(\u0026#34;hugo PersistentPreRunE\u0026#34;) return nil }, PreRun: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;hugo PreRun\u0026#34;) }, PreRunE: func(cmd *cobra.Command, args []string) error { fmt.Println(\u0026#34;hugo PreRunE\u0026#34;) return errors.New(\u0026#34;PreRunE err\u0026#34;) }, Run: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;run hugo...\u0026#34;) }, PostRun: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;hugo PostRun\u0026#34;) }, PersistentPostRun: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;hugo PersistentPostRun\u0026#34;) }, } 重新编译并运行 hugo 命令：\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 # 编译 $ go build -o hugo # 执行 $ ./hugo hugo PersistentPreRunE hugo PreRunE Error: PreRunE err Usage: hugo [flags] hugo [command] Available Commands: completion Generate the autocompletion script for the specified shell help Help about any command print version Print the version number of Hugo Flags: --author string Author name for copyright attribution (default \u0026#34;YOUR NAME\u0026#34;) -c, --config string config file -h, --help help for hugo -r, --region string AWS region (required) -s, --source string Source directory to read from -v, --verbose verbose output Use \u0026#34;hugo [command] --help\u0026#34; for more information about a command. PreRunE err 可以发现，虽然同时定义了 PersistentPreRun、PersistentPreRunE 两个钩子函数，但只有 PersistentPreRunE 会被执行。\n在执行 PreRunE 时返回了一个错误 PreRunE err，程序会终止运行并打印错误信息。\n如果子命令定义了自己的 Persistent*Run 函数，则不会继承父命令的 Persistent*Run 函数。\n1 2 3 4 5 6 7 8 9 10 11 12 var versionCmd = \u0026amp;cobra.Command{ Use: \u0026#34;version\u0026#34;, PersistentPreRun: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;version PersistentPreRun\u0026#34;) }, PreRun: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;version PreRun\u0026#34;) }, Run: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;Hugo Static Site Generator v0.9 -- HEAD\u0026#34;) }, } 重新编译并运行 hugo 命令：\n1 2 3 4 5 6 7 8 # 编译 $ go build -o hugo # 执行子命令 $ ./hugo version version PersistentPreRun version PreRun Hugo Static Site Generator v0.9 -- HEAD hugo PersistentPostRun 注意事项：cobra.OnInitialize()传入调用命令时执行的函数（可以用来初始化配置）：执行顺序都在Hooks函数的前面。\n定义自己的 Help 命令 如果你对 Cobra 自动生成的帮助命令不满意，我们可以自定义帮助命令或模板。\n1 2 3 cmd.SetHelpCommand(cmd *Command) cmd.SetHelpFunc(f func(*Command, []string)) cmd.SetHelpTemplate(s string) Cobra 提供了三个方法来实现自定义帮助命令。\n默认情况下，我们可以使用 hugo help command 语法查看子命令的帮助信息，也可以使用 hugo command -h/--help 查看。\n使用 help 命令查看帮助信息：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ ./hugo help version hugo PersistentPreRunE All software has versions. This is Hugo\u0026#39;s Usage: hugo version [flags] Flags: -h, --help help for version Global Flags: --author string Author name for copyright attribution (default \u0026#34;YOUR NAME\u0026#34;) -v, --verbose verbose output hugo PersistentPostRun 使用 -h/--help 查看帮助信息：\n1 2 3 4 5 6 7 8 9 10 11 12 $ ./hugo version -h All software has versions. This is Hugo\u0026#39;s Usage: hugo version [flags] Flags: -h, --help help for version Global Flags: --author string Author name for copyright attribution (default \u0026#34;YOUR NAME\u0026#34;) -v, --verbose verbose output 二者唯一的区别是，使用 help 命令查看帮助信息时会执行初始化函数（这里没有演示，参考）和钩子函数。\n我们可以使用 rootCmd.SetHelpCommand 来控制 help 命令输出，使用 rootCmd.SetHelpFunc 来控制 -h/--help 输出。\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 func init() { rootCmd.PersistentFlags().BoolVarP(\u0026amp;Verbose, \u0026#34;verbose\u0026#34;, \u0026#34;v\u0026#34;, false, \u0026#34;Verbose output\u0026#34;) rootCmd.Flags().StringVarP(\u0026amp;Source, \u0026#34;source\u0026#34;, \u0026#34;s\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;Source directory to read from\u0026#34;) rootCmd.Flags().StringVarP(\u0026amp;Region, \u0026#34;region\u0026#34;, \u0026#34;r\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;AWS region (required)\u0026#34;) _ = rootCmd.MarkFlagRequired(\u0026#34;region\u0026#34;) rootCmd.PersistentFlags().StringVar(\u0026amp;Author, \u0026#34;author\u0026#34;, \u0026#34;YOUR NAME\u0026#34;, \u0026#34;Author name for copyright attribution\u0026#34;) _ = viper.BindPFlag(\u0026#34;author\u0026#34;, rootCmd.PersistentFlags().Lookup(\u0026#34;author\u0026#34;)) cobra.OnInitialize(initConfig) rootCmd.Flags().StringVarP(\u0026amp;cfgFile, \u0026#34;config\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;config file\u0026#34;) rootCmd.SetHelpCommand(\u0026amp;cobra.Command{ Use: \u0026#34;help\u0026#34;, Short: \u0026#34;Custom help command\u0026#34;, Hidden: true, Run: func(cmd *cobra.Command, args []string) { fmt.Println(\u0026#34;Custom help command\u0026#34;) }, }) rootCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { fmt.Println(strings) }) } 重新编译并运行 hugo 命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 编译 $ go build -o hugo # 使用 `help` 命令查看帮助信息 $ ./hugo help version hugo PersistentPreRunE Custom help command hugo PersistentPostRun # 使用 `-h` 查看根命令帮助信息 $ ./hugo -h [-h] # 使用 `-h` 查看 version 命令帮助信息 $ ./hugo version -h [version -h] 可以发现，使用 help 命令查看帮助信息输出结果是 rootCmd.SetHelpCommand 中 Run 函数的执行输出。使用 -h 查看帮助信息输出结果是 rootCmd.SetHelpFunc 函数的执行输出，strings 代表的是命令行标志和参数列表。\n现在我们再来测试下 rootCmd.SetHelpTemplate 的作用，它用来设置帮助信息模板，支持标准的 Go Template 语法，自定义模板如下：\n1 2 3 4 5 6 7 8 9 10 rootCmd.SetHelpTemplate(`Custom Help Template: Usage: {{.UseLine}} Description: {{.Short}} Commands: {{- range .Commands}} {{.Name}}: {{.Short}} {{- end}} `) 注意：为了单独测试 cmd.SetHelpTemplate(s string)，我已将上面 rootCmd.SetHelpCommand 和 rootCmd.SetHelpFunc 部分代码注释掉了。\n重新编译并运行 hugo 命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 编译 $ go build -o hugo # 查看帮助 $ ./hugo -h Custom Help Template: Usage: hugo [flags] Description: Hugo is a very fast static site generator Commands: completion: Generate the autocompletion script for the specified shell help: Help about any command print: version: Print the version number of Hugo # 查看子命令帮助 $ ./hugo help version hugo PersistentPreRunE Custom Help Template: Usage: hugo version [flags] Description: Print the version number of Hugo Commands: hugo PersistentPostRun 可以发现，无论使用 help 命令查看帮助信息，还是使用 -h 查看帮助信息，其输出内容都遵循我们自定义的模版格式。\n定义自己的 Usage Message 当用户提供无效标志或无效命令时，Cobra 通过向用户显示 Usage 来提示用户如何正确的使用命令。\n例如，当用户输入无效的标志 --demo 时，将得到如下输出：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $ ./hugo --demo Error: unknown flag: --demo Usage: hugo [flags] hugo [command] Available Commands: completion Generate the autocompletion script for the specified shell help Help about any command print version Print the version number of Hugo Flags: --author string Author name for copyright attribution (default \u0026#34;YOUR NAME\u0026#34;) -c, --config string config file -h, --help help for hugo -s, --source string Source directory to read from -v, --verbose verbose output Use \u0026#34;hugo [command] --help\u0026#34; for more information about a command. unknown flag: --demo 首先程序会报错 Error: unknown flag: --demo，报错后会显示 Usage 信息。\n这个输出格式默认与 help 信息一样，我们也可以进行自定义。Cobra 提供了如下两个方法，来控制输出，具体效果我就不演示了，留给读者自行探索。\n1 2 cmd.SetUsageFunc(f func(*Command) error) cmd.SetUsageTemplate(s string) 未知命令建议 在我们使用 git 命令时，有一个非常好用的功能，能够对用户输错的未知命令智能提示。\n示例如下：\n1 2 3 4 5 6 7 $ git statu git: \u0026#39;statu\u0026#39; is not a git command. See \u0026#39;git --help\u0026#39;. The most similar commands are status stage stash 当我们输入一个不存在的命令 statu 时，git 会提示命令不存在，并且给出几个最相似命令的建议。\n这个功能非常实用，幸运的是，Cobra 自带了此功能。\n如下，当我们输入一个不存在的命令 vers 时，hugo 会自动给出建议命令 version：\n1 2 3 4 5 6 7 8 9 10 11 $ ./hugo vers Error: unknown command \u0026#34;vers\u0026#34; for \u0026#34;hugo\u0026#34; Did you mean this? version Run \u0026#39;hugo --help\u0026#39; for usage. unknown command \u0026#34;vers\u0026#34; for \u0026#34;hugo\u0026#34; Did you mean this? version 注意⚠️：根据我的实测，要想让此功能生效，Command.TraverseChildren 属性要置为 false。\n如果你想彻底关闭此功能，可以使用如下设置：\n1 cmd.DisableSuggestions = true 或者使用如下设置调整字符串匹配的最小距离：\n1 cmd.SuggestionsMinimumDistance = 1 SuggestionsMinimumDistance 是一个正整数，表示输错的命令与正确的命令最多有几个不匹配的字符（最小距离），才会给出建议。如当值为 1 时，用户输入 hugo versiox 会给出建议，而如果用户输入 hugo versixx 时，则不会给出建议，因为已经有两个字母不匹配 version 了。\nShell 补全 本文在讲添加子命令小节时，我们见到过 completion 子命令，可以为指定的 Shell 生成自动补全脚本，现在我们就来讲解它的用法。\n直接执行 hugo completion -h 命令，我们可以查看它支持的几种 Shell 类型 bash、fish、powershell、zsh。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $ go run main.go completion -h Generate the autocompletion script for tools for the specified shell. See each sub-command\u0026#39;s help for details on how to use the generated script. Usage: tools completion [command] Available Commands: bash Generate the autocompletion script for bash fish Generate the autocompletion script for fish powershell Generate the autocompletion script for powershell zsh Generate the autocompletion script for zsh Flags: -h, --help help for completion Global Flags: --author string Author name for copyright attribution (default \u0026#34;YOUR NAME\u0026#34;) -v, --verbose Verbose output Use \u0026#34;tools completion [command] --help\u0026#34; for more information about a command. 要想知道自己正在使用的 Shell 类型，可以使用如下命令：\n1 2 3 4 $ echo $0 /bin/zsh $ echo $SHELL /bin/zsh 可以发现，我使用的是 zsh，所以我就以 zsh 为例，来演示下 completion 命令补全用法。\n使用 -h/--help 我们可以查看使用说明：\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 $ ./hugo completion zsh -h Generate the autocompletion script for the zsh shell. If shell completion is not already enabled in your environment you will need to enable it. You can execute the following once: echo \u0026#34;autoload -U compinit; compinit\u0026#34; \u0026gt;\u0026gt; ~/.zshrc To load completions in your current shell session: source \u0026lt;(hugo completion zsh) To load completions for every new session, execute once: #### Linux: hugo completion zsh \u0026gt; \u0026#34;${fpath[1]}/_hugo\u0026#34; #### macOS: hugo completion zsh \u0026gt; $(brew --prefix)/share/zsh/site-functions/_hugo You will need to start a new shell for this setup to take effect. Usage: hugo completion zsh [flags] Flags: -h, --help help for zsh --no-descriptions disable completion descriptions Global Flags: --author string Author name for copyright attribution (default \u0026#34;YOUR NAME\u0026#34;) -v, --verbose verbose output 根据帮助信息，如果为当前会话提供命令行补全功能，可以使用 source \u0026lt;(hugo completion zsh) 命令来实现。\n如果要让命令行补全功能永久生效，Cobra 则非常贴心的为 Linux 和 macOS 提供了不同命令。\n你可以根据提示选择自己喜欢的方式来实现命令行补全功能。\n我这里只实现为当前会话提供命令行补全功能为例进行演示：\n1 2 3 4 5 6 7 8 9 10 11 # 首先在项目根目录下，安装 hugo 命令行程序，安装后软件存放在 $GOPATH/bin 目录下 $ go install . # 添加命令行补全功能 $ source \u0026lt;(hugo completion zsh) # 现在命令行补全已经生效，只需要输入一个 `v`，然后按下键盘上的 `Tab` 键，命令将自动补全为 `version` $ hugo v # 命令已被自动补全 $ hugo version version PersistentPreRun version PreRun Hugo Static Site Generator v0.9 -- HEAD 其实将命令 source \u0026lt;(hugo completion zsh) 添加到 ~/.zshrc 文件中，也能实现每次进入 zsh 后自动加载 hugo 的命令行补全功能。\n注意：在执行 source \u0026lt;(hugo completion zsh) 前需要将 rootCmd 中的钩子函数内部的 fmt.Println 代码全部注释掉，不然打印内容会被当作命令来执行，将会得到 Error: unknown command \u0026quot;PersistentPreRunE\u0026quot; for \u0026quot;hugo\u0026quot; 类似报错信息，虽然命令行补全功能依然能够生效，但「没有消息才是最好的消息」。\n还有要注意：主命令要与项目名（构建之后的命令）一直，不然也不会自动补全。\n生成文档 Cobra 支持生成 Markdown、ReStructured Text、Man Page 三种格式文档。\n这里以生成 Markdown 格式文档为例，来演示下 Cobra 这一强大功能。\n我们可以定义一个标志 md-docs 来决定是否生成文档：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // hugo/cmd/root.go ... import \u0026#34;github.com/spf13/cobra/doc\u0026#34; ... var MarkdownDocs bool func init() { rootCmd.Flags().BoolVarP(\u0026amp;MarkdownDocs, \u0026#34;md-docs\u0026#34;, \u0026#34;m\u0026#34;, false, \u0026#34;gen Markdown docs\u0026#34;) ... } func GenDocs() { if MarkdownDocs { if err := doc.GenMarkdownTree(rootCmd, \u0026#34;./docs/md\u0026#34;); err != nil { fmt.Println(err) os.Exit(1) } } } 在 main.go 中调用 GenDocs() 函数。注意./docs/md路径必须存在。\n1 2 3 4 func main() { cmd.Execute() cmd.GenDocs() } 现在，重新编译并运行 hugo 即可生成文档：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 编译 $ go build -o hugo # 生成文档 $ ./hugo --md-docs ... ... # 会执行root命令的Run函数 # 查看生成的文档 $ tree docs/md docs/md ├── hugo.md ├── hugo_completion.md ├── hugo_completion_bash.md ├── hugo_completion_fish.md ├── hugo_completion_powershell.md ├── hugo_completion_zsh.md ├── hugo_print.md └── hugo_version.md 可以发现，Cobra 不仅为 hugo 命令生成了文档，并且还生成了子命令的文档以及命令行补全的文档。\n使用 Cobra 命令创建项目 文章读到这里，我们可以发现，其实 Cobra 项目是遵循一定套路的，目录结构、文件、模板代码都比较固定。\n此时，脚手架工具就派上用场了。Cobra 提供了 cobra-cli 命令行工具，可以通过命令的方式快速创建一个命令行项目。\n安装：\n1 $ go install github.com/spf13/cobra-cli@latest 查看使用帮助：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $ cobra-cli -h Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application. Usage: cobra-cli [command] Available Commands: add Add a command to a Cobra Application completion Generate the autocompletion script for the specified shell help Help about any command init Initialize a Cobra Application Flags: -a, --author string author name for copyright attribution (default \u0026#34;YOUR NAME\u0026#34;) --config string config file (default is $HOME/.cobra.yaml) -h, --help help for cobra-cli -l, --license string name of license for the project --viper use Viper for configuration Use \u0026#34;cobra-cli [command] --help\u0026#34; for more information about a command. 可以发现，cobra-cli 脚手架工具仅提供了少量命令和标志，所以上手难度不大。\n初始化模块 要使用 cobra-cli 生成一个项目，首先要手动创建项目根目录并使用 go mod 命令进行初始化。\n假设我们要编写的命令行程序叫作 cob，模块初始化过程如下：\n1 2 3 4 5 6 # 创建项目目录 $ mkdir cob # 进入项目目录 $ cd cob # 初始化模块 $ go mod init github.com/arlettebrook/cob 初始化命令行程序 有了初始化好的 Go 项目，我们就可以初始化命令行程序了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 初始化程序 $ cobra-cli init Your Cobra application is ready at ... # 查看生成的项目目录结构 $ tree . . ├── LICENSE ├── cmd │ └── root.go ├── go.mod ├── go.sum └── main.go 2 directories, 5 files # 执行命令行程序 $ go run main.go A longer description that spans multiple lines and likely contains examples and usage of using your application. For example: Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application. 使用 cobra-cli 初始化程序非常方便，只需要一个简单的 init 命令即可完成。\n目录结构跟我们手动编写的程序相同，只不过多了一个 LICENSE 文件，用来存放项目的开源许可证。\n通过 go run main.go 执行这个命令行程序，即可打印 rootCmd.Run 的输出结果。\n使用脚手架自动生成的 cob/main.go 文件内容如下：\n1 2 3 4 5 6 7 8 9 10 11 /* Copyright © 2024 NAME HERE \u0026lt;EMAIL ADDRESS\u0026gt; */ package main import \u0026#34;github.com/arlettebrook/cob/cmd\u0026#34; func main() { cmd.Execute() } 自动生成的 cog/cmd/root.go 文件内容如下：\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 45 46 47 48 49 /* Copyright © 2023 NAME HERE \u0026lt;EMAIL ADDRESS\u0026gt; */ package cmd import ( \u0026#34;os\u0026#34; \u0026#34;github.com/spf13/cobra\u0026#34; ) // rootCmd represents the base command when called without any subcommands var rootCmd = \u0026amp;cobra.Command{ Use: \u0026#34;cog\u0026#34;, Short: \u0026#34;A brief description of your application\u0026#34;, Long: `A longer description that spans multiple lines and likely contains examples and usage of using your application. For example: Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application.`, // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { err := rootCmd.Execute() if err != nil { os.Exit(1) } } func init() { // Here you will define your flags and configuration settings. // Cobra supports persistent flags, which, if defined here, // will be global for your application. // rootCmd.PersistentFlags().StringVar(\u0026amp;cfgFile, \u0026#34;config\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;config file (default is $HOME/.cog.yaml)\u0026#34;) // Cobra also supports local flags, which will only run // when this action is called directly. rootCmd.Flags().BoolP(\u0026#34;toggle\u0026#34;, \u0026#34;t\u0026#34;, false, \u0026#34;Help message for toggle\u0026#34;) } 以上两个文件跟我们手动编写的代码没什么两样，套路完全相同，唯一不同的是每个文件头部都会多出来一个 Copyright 头信息，用来标记代码的 LICENSE。\n可选标志 cobra-cli 提供了如下三个标志分别用来设置项目的作者、许可证类型、是否使用 Viper 管理配置。\n1 2 3 $ cobra-cli init --author arlettebrook --license mit --viper Your Cobra application is ready at ... 以上命令我们指定可选标志后对项目进行了重新初始化。\n现在 LICENSE 文件内容不再为空，而是 MIT 协议。\n1 2 3 4 5 The MIT License (MIT) Copyright © 2024 arlettebrook Permission is hereby granted... 并且 Go 文件 Copyright 头信息中作者信息也会被补全。\n1 2 3 4 5 /* Copyright © 2024 arlettebrook ... */ 笔记：cobra-cli 命令内置开源许可证支持 GPLv2、GPLv3、LGPL、AGPL、MIT、2-Clause BSD 或 3-Clause BSD。也可以参考官方文档来指定自定义许可证。\n提示：如果你对开源许可证不熟悉，可以参考我的另一篇文章《Open Source License Introduction》。\n添加命令 1 2 3 $ cobra-cli add serve $ cobra-cli add config $ cobra-cli add create -p configCmd --author arlettebrook --license mit --viper 这里分别添加了三个命令 serve、config、create，前两者都是 rootCmd 的子命令，create 命令则通过 -p 'configCmd' 参数指定为 config 的子命令。\n注意⚠️：使用 -p 'configCmd' 标志指定当前命令的父命令时，configCmd 必须是小驼峰命名法，因为 cobra-cli 为 config 生成的命令代码自动命名为 configCmd，而不是 config_cmd 或其他形式，这符合 Go 语言变量命名规范。\n现在命令行程序目录结构如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 $ tree . . ├── LICENSE ├── cmd │ ├── config.go │ ├── create.go │ ├── root.go │ └── serve.go ├── go.mod ├── go.sum └── main.go 2 directories, 8 files 可以使用如下命令执行子命令：\n1 2 $ go run main.go config create create called 其他新添加的命令同理。\n使用配置取代标志 如果你不想每次生成或添加命令时都指定选项参数，则可以定义 ~/.cobra.yaml 文件来保存配置信息：\n1 2 3 4 author: arlettebrook \u0026lt;arlettebrook@proton.me\u0026gt; year: 2024 license: MIT useViper: true 再次使用 init 命令初始化程序：\n1 2 $ cobra-cli init Using config file: C:\\Users\\Lenovo\\.cobra.yaml 会提示使用了 ~/.cobra.yaml 配置文件。\n现在 LICENSE 文件内容格式如下：\n1 2 3 4 5 The MIT License (MIT) Copyright © 2024 arlettebrook \u0026lt;arlettebrook@proton.me\u0026gt; ... Go 文件 Copyright 头信息也会包含日期、用户名、用户邮箱。\n1 2 3 4 5 /* Copyright © 2024 arlettebrook \u0026lt;arlettebrook@proton.me\u0026gt; ... */ 如果你不想把配置保存在 ~/.cobra.yaml 中，cobra-cli 还提供了 --config 标志来指定任意目录下的配置文件。\n至此，cobra-cli 的功能我们就都讲解完成了，还是非常方便实用的。\n总结 在我们日常开发中，编写命令行程序是必不可少，很多开源软件都具备强大的命令行工具，如 K8s、Docker、Git 等。\n一款复杂的命令行程序通常有上百种使用组合，所以如何组织和编写出好用的命令行程序是很考验开发者功底的，而 Cobra 则为我们开发命令行程序提供了足够的便利。这也是为什么我将其称为命令行框架，而不仅仅是一个 Go 第三方库。\nCobra 功能非常强大，要使用它来编写命令行程序首先要明白三个概念：命令、参数和标志。\nCobra 不仅支持子命令，还能够完美兼容 pflag 和 Viper 包，因为这三个包都是同一个作者开发的。关于标志，Cobra 支持持久标志、本地标志以及将标志标记为必选。Cobra 可以将标志绑定到 Viper，方便使用 viper.Get() 来获取标志的值。对于命令行参数，Cobra 提供了不少验证函数，我们也可以自定义验证函数。\nCobra 还提供了几个 Hooks 函数 PersistentPreRun、PreRun、PostRun、PersistentPostRun，可以分别在执行 Run 前后来处理一段逻辑。\n如果觉得 Cobra 提供的默认帮助信息不能满足需求，我们还可以定义自己的 Help 命令和 Usage Message，非常灵活。\nCobra 还支持未知命令的智能提示功能以及 Shell 自动补全功能，此外，它还支持自动生成 Markdown、ReStructured Text、Man Page 三种格式的文档。这对命令行工具的使用者来说非常友好，还能极大减少开发者的工作量。\n最后，Cobra 的命令行工具 cobra-cli 进一步提高了编写命令行程序的效率，非常推荐使用。\n本文完整 代码 (参考@jianghushinian），欢迎点击查看。\n参考 Cobra 官网：https://cobra.dev/ Cobra 源码：https://github.com/spf13/cobra Cobra 文档：https://pkg.go.dev/github.com/spf13/cobra Cobra-CLI 文档：https://github.com/spf13/cobra-cli/blob/main/README.md 原文地址：Go 语言现代命令行框架 Cobra 详解 ","date":"2024-05-02T23:33:49+08:00","permalink":"https://arlettebrook.github.io/p/go%E6%9E%84%E5%BB%BA%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%A8%8B%E5%BA%8F%E4%B9%8B%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93cobra/","title":"Go构建命令行程序之第三方库cobra"},{"content":" 在使用 Go 进行开发的过程中，命令行参数解析是我们经常遇到的需求。尽管 Go 标准库提供了 flag 包用于实现命令行参数解析，但只能满足基本需要，不支持高级特性。于是 Go 社区中出现了一个叫 pflag 的第三方包，功能更加全面且足够强大。在本文中，我们将学习并掌握如何使用 pflag。\n特点 pflag 作为 Go 内置 flag 包的替代品，具有如下特点：\n实现了 POSIX/GNU 风格的 –flags。 pflag 与《The GNU C Library》 中「25.1.1 程序参数语法约定」章节中 POSIX 建议语法兼容。 兼容 Go 标准库中的 flag 包。如果直接使用 flag 包定义的全局 FlagSet 对象 CommandLine，则完全兼容；否则当你手动实例化了 FlagSet 对象，这时就需要为每个标志设置一个简短标志（Shorthand）。 使用 基本用法 安装\n1 go get -u github.com/spf13/pflag 导入\n1 import \u0026#34;github.com/spf13/pflag\u0026#34; 使用\n我们可以像使用 Go 标准库中的 flag 包一样使用 pflag。\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 45 46 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/pflag\u0026#34; ) type host struct { value string } func (h *host) String() string { return h.value } func (h *host) Set(v string) error { h.value = v return nil } func (h *host) Type() string { return \u0026#34;host\u0026#34; } func main() { var ip *int = pflag.Int(\u0026#34;ip\u0026#34;, 1234, \u0026#34;help message for ip\u0026#34;) var port int pflag.IntVar(\u0026amp;port, \u0026#34;port\u0026#34;, 8080, \u0026#34;help message for port\u0026#34;) var h host pflag.Var(\u0026amp;h, \u0026#34;host\u0026#34;, \u0026#34;help message for host\u0026#34;) // 解析命令行参数 pflag.Parse() fmt.Printf(\u0026#34;ip: %d\\n\u0026#34;, *ip) fmt.Printf(\u0026#34;port: %d\\n\u0026#34;, port) fmt.Printf(\u0026#34;host: %+v\\n\u0026#34;, h) fmt.Printf(\u0026#34;NFlag: %v\\n\u0026#34;, pflag.NFlag()) // 返回已设置的命令行标志个数 fmt.Printf(\u0026#34;NArg: %v\\n\u0026#34;, pflag.NArg()) // 返回处理完标志后剩余的参数个数 fmt.Printf(\u0026#34;Args: %v\\n\u0026#34;, pflag.Args()) // 返回处理完标志后剩余的参数列表 fmt.Printf(\u0026#34;Arg(1): %v\\n\u0026#34;, pflag.Arg(1)) // 返回处理完标志后剩余的参数列表中第 i 项 } 以上示例演示的 pflag 用法跟 flag 包用法一致，可以做到二者无缝替换。\n示例分别使用 pflag.Int()、pflag.IntVar()、pflag.Var() 三种不同方式来声明标志。其中 ip 和 port 都是 int 类型标志，host 标志则为自定义的 host 类型，它实现了 pflag.Value 接口，通过实现接口类型，标志能够支持任意类型，增加灵活性。\n通过 --help/-h 参数查看命令行程序使用帮助：\n1 2 3 4 5 6 7 $ go run main.go --help Usage of ./main: --host host help message for host --ip int help message for ip (default 1234) --port int help message for port (default 8080) pflag: help requested exit status 2 可以发现，帮助信息中的标志位置是经过重新排序的，并不是标志定义的顺序。\n与 flag 包不同的是，pflag 包参数定界符是两个 -，而不是一个 -，在 pflag 中 -- 和 - 具有不同含义，这点稍后会进行介绍。\nip 标志的默认参数为 1234，port 标志的默认参数为 8080。\n注意：在有些终端下执行程序退出后，还会多打印一行 exit status 2，这并不意味着程序没有正常退出，而是因为 --help 意图就是用来查看使用帮助，所以程序在打印使用帮助信息后，主动调用 os.Exit(2) 退出了。\n通过如下方式使用命令行程序：\n1 2 3 4 5 6 7 8 $ go run main.go --ip 1 x y --host localhost a b ip: 1 port: 8080 host: {value:localhost} NFlag: 2 NArg: 4 Args: [x y a b] Arg(1): y ip 标志的默认值已被命令行参数 1 所覆盖，由于没有传递 port 标志，所以打印结果为默认值 8080，host 标志的值也能够被正常打印。\n还有 4 个非选项参数数 x、y、a、b 也都被 pflag 识别并记录了下来。这点比 flag 要强大，在 flag 包中，非选项参数数只能写在所有命令行参数最后，x、y 出现在这里程序是会报错的。\n进阶用法 除了像 flag 包一样的用法，pflag 还支持一些独有的用法，以下是用法示例。\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 45 46 47 48 49 50 51 52 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/spf13/pflag\u0026#34; ) type host struct { value string } func (h *host) String() string { return h.value } func (h *host) Set(v string) error { h.value = v return nil } func (h *host) Type() string { return \u0026#34;host\u0026#34; } func main() { flagSet := pflag.NewFlagSet(\u0026#34;test\u0026#34;, pflag.ExitOnError) var ip = flagSet.IntP(\u0026#34;ip\u0026#34;, \u0026#34;i\u0026#34;, 1234, \u0026#34;help message for ip\u0026#34;) var boolVar bool flagSet.BoolVarP(\u0026amp;boolVar, \u0026#34;boolVar\u0026#34;, \u0026#34;b\u0026#34;, true, \u0026#34;help message for boolVar\u0026#34;) var h host flagSet.VarP(\u0026amp;h, \u0026#34;host\u0026#34;, \u0026#34;H\u0026#34;, \u0026#34;help message for host\u0026#34;) flagSet.SortFlags = false parseErr := flagSet.Parse(os.Args[1:]) if parseErr != nil { log.Fatal(\u0026#34;解析命令行参数出错：\u0026#34;, parseErr) } fmt.Printf(\u0026#34;ip: %d\\n\u0026#34;, *ip) fmt.Printf(\u0026#34;boolVar: %t\\n\u0026#34;, boolVar) fmt.Printf(\u0026#34;host: %#v\\n\u0026#34;, h) i, err := flagSet.GetInt(\u0026#34;ip\u0026#34;) fmt.Printf(\u0026#34;i: %d, err: %v\\n\u0026#34;, i, err) } 首先我们通过 pflag.NewFlagSet 自定义了 FlagSet 对象 flagset，之后的标志定义和解析都通过 flagset 来完成。\n前文示例中 pflag.Int() 这种用法，实际上使用的是全局 FlagSet 对象 CommandLine，CommandLine 定义如下：\n1 var CommandLine = NewFlagSet(os.Args[0], ExitOnError) 现在同样使用三种不同方式来声明标志，分别为 flagset.IntP()、flagset.BoolVarP()、flagset.VarP()。不难发现，这三个方法的命名结尾都多了一个 P，它们的能力也得以升级，三个方法都多了一个 shorthand string 参数（flagset.IntP 的第 2 个参数，flagset.BoolVarP 和 flagset.VarP 的第 3 个参数）用来设置简短标志。\n从声明标志的方法名中我们能够总结出一些规律：\npflag.\u0026lt;Type\u0026gt; 类方法名会将标志参数值存储在指针中并返回。 pflag.\u0026lt;Type\u0026gt;Var 类方法名中包含 Var 关键字的，会将标志参数值绑定到第一个指针类型的参数。 pflag.\u0026lt;Type\u0026gt;P、pflag.\u0026lt;Type\u0026gt;VarP 类方法名以 P 结尾的，支持简短标志。 一个完整标志在命令行传参时使用的分界符为 --，而一个简短标志的分界符则为 -。\nflagset.SortFlags = false 作用是禁止打印帮助信息时对标志进行重排序。\n示例最后，使用 flagset.GetInt() 获取参数的值。\n通过 --help/-h 参数查看命令行程序使用帮助：\n1 2 3 4 5 6 7 $ go run main.go --help Usage of test: -i, --ip int help message for ip (default 1234) -b, --boolVar help message for boolVar (default true) -H, --host host help message for host pflag: help requested exit status 2 这次的帮助信息中，标志顺序没有被改变，就是声明的顺序。\n每一个标志都会对应一个简短标志，如 -b 和 --boolVar 是等价的，可以更加方便的设置参数。\n指定如下命令行参数运行示例：\n1 2 3 4 5 $ go run main.go --ip 1 -H localhost --boolVar=false ip: 1 boolVar: false host: main.host{value:\u0026#34;localhost\u0026#34;} i: 1, err: \u0026lt;nil\u0026gt; 通过 --ip 1 使用完整标志指定 ip 参数值。\n通过 -H localhost 使用简短标志指定 host 参数值。\n布尔类型的标志指定参数 --boolVar=false 需要使用等号 = 而非空格。\n命令行标志语法 命令行标志遵循如下语法：\n语法 说明 --flag 适用于 bool 类型标志，或具有 NoOptDefVal 属性的标志。 --flag x 适用于非 bool 类型标志，或没有 NoOptDefVal 属性的标志。 --flag=x 适用于 bool 类型标志。 -n 1234/-n=1234/-n1234 简短标志，非 bool 类型且没有 NoOptDefVal 属性，三者等价。 标志解析在终止符 -- 之后停止。\n整数标志接受 1234、0664、0x1234，并且可能为负数。\n布尔标志接受 1, 0, t, f, true, false, TRUE, FALSE, True, False。\nDuration 标志接受任何对 time.ParseDuration 有效的输入。\n标志名 Normalize 标准化Normalize\n借助 pflag.NormalizedName 我们能够给标志起一个或多个别名、规范化标志名等。\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;github.com/spf13/pflag\u0026#34; ) func normalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName { // alias switch name { case \u0026#34;old-flag-name\u0026#34;: name = \u0026#34;new-flag-name\u0026#34; break } // --my-flag == --my_flag == --my.flag from := []string{\u0026#34;-\u0026#34;, \u0026#34;_\u0026#34;} to := \u0026#34;.\u0026#34; for _, sep := range from { name = strings.Replace(name, sep, to, -1) } return pflag.NormalizedName(name) } func main() { flagSet := pflag.NewFlagSet(\u0026#34;test\u0026#34;, pflag.ExitOnError) var ip = flagSet.IntP(\u0026#34;new-flag-name\u0026#34;, \u0026#34;i\u0026#34;, 1234, \u0026#34;help message for new-flag-name\u0026#34;) var myFlag = flagSet.IntP(\u0026#34;my-flag\u0026#34;, \u0026#34;m\u0026#34;, 1234, \u0026#34;help message for my-flag\u0026#34;) flagSet.SetNormalizeFunc(normalizeFunc) err := flagSet.Parse(os.Args[1:]) if err != nil { log.Fatal(\u0026#34;命令行参数解析失败：\u0026#34;, err) } fmt.Printf(\u0026#34;ip: %d\\n\u0026#34;, *ip) fmt.Printf(\u0026#34;myFlag: %d\\n\u0026#34;, *myFlag) } 要使用 pflag.NormalizedName，我们需要创建一个函数 normalizeFunc，然后将其通过 flagset.SetNormalizeFunc(normalizeFunc) 注入到 flagset 使其生效。\n在 normalizeFunc 函数中，我们给 new-flag-name 标志起了一个别名 old-flag-name。\n另外，还对标志名进行了规范化处理，带有 - 和 _ 分割符的标志名，会统一规范化成以 . 作为分隔符的标志名。\n使用示例如下：\n1 2 3 4 5 6 7 8 9 10 11 $ go run pflag.go --old-flag-name 2 --my-flag 200 ip: 2 myFlag: 200 $ go run pflag.go --new-flag-name 3 --my_flag 300 ip: 3 myFlag: 300 $ go run pflag.go --new-flag_name 2 --my.flag 200 ip: 2 myFlag: 200 NoOptDefVal NoOptDefVal 是 no option default values 的简写。\n创建标志后，可以为标志设置 NoOptDefVal 属性，如果标志具有 NoOptDefVal 属性并且在命令行上设置了标志而没有参数选项，则标志将设置为 NoOptDefVal 指定的值。\n如下示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/pflag\u0026#34; ) func main() { var ip = pflag.IntP(\u0026#34;flagName\u0026#34;, \u0026#34;f\u0026#34;, 1234, \u0026#34;help message\u0026#34;) pflag.Lookup(\u0026#34;flagName\u0026#34;).NoOptDefVal = \u0026#34;4321\u0026#34; pflag.Parse() fmt.Println(*ip) } 不同参数结果如下：\n命令行参数 结果值 –flagname=1357 ip=1357 –flagname ip=4321 [nothing] ip=1234 1 2 3 4 5 var ip = pflag.BoolP(\u0026#34;flagName\u0026#34;, \u0026#34;f\u0026#34;, false, \u0026#34;help message\u0026#34;) //pflag.Lookup(\u0026#34;flagName\u0026#34;).NoOptDefVal = \u0026#34;false\u0026#34; pflag.Parse() fmt.Println(*ip) bool类型的NoOptDefValue默认值是true，可以修改为false\n1 2 $ go run main.go -f true 弃用/隐藏标志 使用 flags.MarkDeprecated 可以弃用一个标志，使用 flags.MarkShorthandDeprecated 可以弃用一个简短标志，使用 flags.MarkHidden 可以隐藏一个标志。\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/spf13/pflag\u0026#34; ) func main() { flags := pflag.NewFlagSet(\u0026#34;test\u0026#34;, pflag.ExitOnError) var ip = flags.IntP(\u0026#34;ip\u0026#34;, \u0026#34;i\u0026#34;, 1234, \u0026#34;help message for ip\u0026#34;) var boolVar bool flags.BoolVarP(\u0026amp;boolVar, \u0026#34;boolVar\u0026#34;, \u0026#34;b\u0026#34;, true, \u0026#34;help message for boolVar\u0026#34;) var h string flags.StringVarP(\u0026amp;h, \u0026#34;host\u0026#34;, \u0026#34;H\u0026#34;, \u0026#34;127.0.0.1\u0026#34;, \u0026#34;help message for host\u0026#34;) // 弃用标志 _ = flags.MarkDeprecated(\u0026#34;ip\u0026#34;, \u0026#34;deprecated\u0026#34;) _ = flags.MarkShorthandDeprecated(\u0026#34;boolVar\u0026#34;, \u0026#34;please use --boolVar only\u0026#34;) // 隐藏标志 _ = flags.MarkHidden(\u0026#34;host\u0026#34;) err := flags.Parse(os.Args[1:]) if err != nil { log.Fatalln(\u0026#34;解析命令行出错了：\u0026#34;, err) } fmt.Printf(\u0026#34;ip: %d\\n\u0026#34;, *ip) fmt.Printf(\u0026#34;boolVar: %t\\n\u0026#34;, boolVar) fmt.Printf(\u0026#34;host: %+v\\n\u0026#34;, h) } 查看使用帮助：\n1 2 3 4 5 $ go run main.go -h Usage of test: --boolVar help message for boolVar (default true) pflag: help requested exit status 2 从打印结果可以发现，弃用标志 ip 时，其对应的简短标志 i 也会跟着被弃用；弃用 boolVar 所对应的简短标志 b 时，boolVar 标志会被保留；host 标志则完全被隐藏。\n指定如下命令行参数运行示例：\n1 2 3 4 5 $ go run main.go --ip 1 --boolVar=false -H localhost Flag --ip has been deprecated, deprecated ip: 1 boolVar: false host: localhost 打印信息中会提示用户 ip 标志已经弃用，不过使用 --ip 1 指定的参数值依然能够生效。\n隐藏的 host 标志使用 -H localhost 指定参数值同样能够生效。\n指定如下命令行参数运行示例：\n1 2 3 4 5 6 $ go run main.go -i 1 -b=false --host localhost Flag --ip has been deprecated, deprecated Flag shorthand -b has been deprecated, please use --boolVar only ip: 1 boolVar: false host: localhost 打印信息中增加了一条简短标志 -b 已被弃用的提示，指定参数值依然生效。\n对于弃用的 ip 标志，使用简短标志形式传惨 -i 1 同样生效。\n支持 flag 类型 由于 pflag 对 flag 包兼容，所以可以在一个程序中混用二者：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package main import ( \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/pflag\u0026#34; ) func main() { var ip *int = pflag.Int(\u0026#34;ip\u0026#34;, 1234, \u0026#34;help message for ip\u0026#34;) var port *int = flag.Int(\u0026#34;port\u0026#34;, 80, \u0026#34;help message for port\u0026#34;) pflag.CommandLine.AddGoFlagSet(flag.CommandLine) pflag.Parse() fmt.Printf(\u0026#34;ip: %d\\n\u0026#34;, *ip) fmt.Printf(\u0026#34;port: %d\\n\u0026#34;, *port) } 其中，ip 标志是使用 pflag.Int() 声明的，port 标志则是使用 flag.Int() 声明的。只需要通过 AddGoFlagSet 方法将 flag.CommandLine 注册到 pflag 中，那么 pflag 就可以使用 flag 中声明的标志集合了。\n运行示例结果如下：\n1 2 3 $ go run main.go --ip 10 --port 8000 ip: 10 port: 8000 总结 本文主要介绍了 Go第三方标志包 pflag 的特点及用法。\n首先介绍了 pflag 的基本使用方法，包括声明标志、解析命令行参数、获取标志值等。接着介绍了 pflag 的进阶用法，例如自定义 FlagSet、使用 pflag.\u0026lt;Type\u0026gt;P 方法来支持简短标志。之后又对命令行标志语法进行了讲解，对于布尔值、非布尔值和简短标志，都有各自不同的语法。我们还讲解了如何借助 pflag.NormalizedName 给标志起一个或多个别名、规范化标志名。然后介绍了 NoOptDefVal 的作用和如何弃用/隐藏标志。最后通过示例演示了如何在一个程序中混用 flag 和 pflag。\n彩蛋：不知道你有没有发现，示例中的 ip 标志的名称其实代表的是 int pointer 而非 Internet Protocol Address。ip 标志源自官方示例，不过我顺势而为又声明了 port、host 标志，算是一个程序中的谐音梗 :)。\n参考 pflag 源码: https://github.com/spf13/pflag pflag 文档: https://pkg.go.dev/github.com/spf13/pflag 程序参数语法约定: https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html Go 命令行参数解析工具 pflag 使用 ","date":"2024-05-02T15:21:19+08:00","permalink":"https://arlettebrook.github.io/p/go%E8%A7%A3%E6%9E%90%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0%E4%B9%8B%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93pflag/","title":"Go解析命令行参数之第三方库pflag"},{"content":" 简介 在Go flag库介绍中，我们介绍了flag库。flag库是用于解析命令行选项的标准库。但是flag有几个缺点：\n不显示支持短选项。当然在Go flag库介绍文章中也提到过可以通过将两个选项共享同一个变量迂回实现，但写起来比较繁琐； 选项变量的定义比较繁琐，每个选项都需要根据类型调用对应的Type或TypeVar函数； 默认只支持有限的数据类型，当前只有基本类型bool/int/uint/string和time.Duration； 为了解决这些问题，出现了不少第三方解析命令行选项的库，今天的主角go-flags就是其中一个。第一次看到go-flags库是在阅读pgweb源码的时候。\ngo-flags提供了比标准库flag更多的选项。它利用结构标签（struct tag）和反射提供了一个方便、简洁的接口。它除了基本的功能，还提供了丰富的特性：\n支持短选项（-v）和长选项（–verbose）； 支持短选项合写，如-aux； 同一个选项可以设置多个值； 支持所有的基础类型和 map 类型，甚至是函数； 支持命名空间和选项组； 等等。 上面只是粗略介绍了go-flags的特性，下面我们依次来介绍。\n快速开始 学习从使用开始！我们先来看看go-flags的基本使用。\n由于是第三方库，使用前需要安装，执行下面的命令安装：\n1 $ go get -u github.com/jessevdk/go-flags 代码中使用import导入该库：\n1 import \u0026#34;github.com/jessevdk/go-flags\u0026#34; 完整示例代码如下：\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 package main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/jessevdk/go-flags\u0026#34; ) type Option struct { Verbose []bool `short:\u0026#34;v\u0026#34; long:\u0026#34;verbose\u0026#34; description:\u0026#34;Show verbose debug message\u0026#34;` } func main() { var opt Option _, parseErr := flags.Parse(\u0026amp;opt) if parseErr != nil { var errPtr *flags.Error // 指针类型实现Error接口 if errors.As(parseErr, \u0026amp;errPtr) { // 断言的类型要是指针 if errors.Is(errPtr.Type, flags.ErrHelp) { return } return } panic(parseErr) return } fmt.Println(opt.Verbose) } 使用go-flags的一般步骤：\n定义选项结构，在结构标签中设置选项信息。通过short和long设置短、长选项名字，description设置帮助信息。命令行传参时，短选项前加-，长选项前加--； 声明选项变量； 调用go-flags的解析方法解析。 解析之后会返回两个参数： 第一个：剩余未解析选项，是string类型的切片，一般忽略。 第二个：error对象 对于帮助信息的error对象我们应该忽略。内部会打印这个错误，造成重复。其他错误抛出panic，并退出 flags.Parse()方法默认选项是Default = HelpFlag | PrintErrors | PassDoubleDash:打印帮助信息、打印错误信息、\u0026ndash;后面的参数不解析。 错误信息是封装到flags.Error结构体里面的ErrorType类型l字段里面的。所以需要断言之后再判断。 编译、运行代码（我的环境是 Win10 + Git Bash）：\n1 $ go build -o main.exe main.go 短选项：\n1 2 $ ./main.exe -v [true] 长选项：\n1 2 $ ./main.exe --verbose [true] 由于Verbose字段是切片类型，每次遇到-v或--verbose都会追加一个true到切片中。\n多个短选项：\n1 2 $ ./main.exe -v -v [true true] 多个长选项：\n1 2 $ ./main.exe --verbose --verbose [true true] 短选项 + 长选项：\n1 2 $ ./main.exe -v --verbose -v [true true true] 短选项合写：\n1 2 $ ./main.exe -vvv [true true true] 基本特性 支持丰富的数据类型 go-flags相比标准库flag支持更丰富的数据类型：\n所有的基本类型（包括有符号整数int/int8/int16/int32/int64，无符号整数uint/uint8/uint16/uint32/uint64，浮点数float32/float64，布尔类型bool和字符串string）和它们的切片； map 类型。只支持键为string，值为基础类型的 map； 函数类型。 如果字段是基本类型的切片，基本解析流程与对应的基本类型是一样的。切片类型选项的不同之处在于，遇到相同的选项时，值会被追加到切片中。而非切片类型的选项，后出现的值会覆盖先出现的值。\n下面来看一个示例：\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 45 46 47 48 49 50 51 52 53 package main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/jessevdk/go-flags\u0026#34; ) type Option struct { IntFlag int `short:\u0026#34;i\u0026#34; long:\u0026#34;int\u0026#34; description:\u0026#34;int flag value\u0026#34;` IntSlice []int `long:\u0026#34;intslice\u0026#34; description:\u0026#34;int slice flag value\u0026#34;` BoolFlag bool `long:\u0026#34;bool\u0026#34; description:\u0026#34;bool flag value\u0026#34;` BoolSlice []bool `long:\u0026#34;boolslice\u0026#34; description:\u0026#34;bool slice flag value\u0026#34;` FloatFlag float64 `long:\u0026#34;float\u0026#34; description:\u0026#34;float64 flag value\u0026#34;` FloatSlice []float64 `long:\u0026#34;floatslice\u0026#34; description:\u0026#34;float64 slice flag value\u0026#34;` StringFlag string `short:\u0026#34;s\u0026#34; long:\u0026#34;string\u0026#34; description:\u0026#34;string flag value\u0026#34;` StringSlice []string `long:\u0026#34;strslice\u0026#34; description:\u0026#34;string slice flag value\u0026#34;` PtrStringSlice []*string `long:\u0026#34;pstrslice\u0026#34; description:\u0026#34;slice of pointer of string flag value\u0026#34;` Call func(string) `long:\u0026#34;call\u0026#34; description:\u0026#34;callback\u0026#34;` IntMap map[string]int `long:\u0026#34;intmap\u0026#34; description:\u0026#34;A map from string to int\u0026#34;` } func main() { var opt Option opt.Call = func(value string) { fmt.Println(\u0026#34;in callback: \u0026#34;, value) } _, parseErr := flags.Parse(\u0026amp;opt) if parseErr != nil { var errPtr *flags.Error if errors.As(parseErr, \u0026amp;errPtr) { return } panic(parseErr) return } fmt.Printf(\u0026#34;int flag: %v\\n\u0026#34;, opt.IntFlag) fmt.Printf(\u0026#34;int slice flag: %v\\n\u0026#34;, opt.IntSlice) fmt.Printf(\u0026#34;bool flag: %v\\n\u0026#34;, opt.BoolFlag) fmt.Printf(\u0026#34;bool slice flag: %v\\n\u0026#34;, opt.BoolSlice) fmt.Printf(\u0026#34;float flag: %v\\n\u0026#34;, opt.FloatFlag) fmt.Printf(\u0026#34;float slice flag: %v\\n\u0026#34;, opt.FloatSlice) fmt.Printf(\u0026#34;string flag: %v\\n\u0026#34;, opt.StringFlag) fmt.Printf(\u0026#34;string slice flag: %v\\n\u0026#34;, opt.StringSlice) fmt.Println(\u0026#34;slice of pointer of string flag: \u0026#34;) for i := 0; i \u0026lt; len(opt.PtrStringSlice); i++ { fmt.Printf(\u0026#34;\\t%d: %v\\n\u0026#34;, i, *opt.PtrStringSlice[i]) } fmt.Printf(\u0026#34;int map: %v\\n\u0026#34;, opt.IntMap) } 基本类型和其切片比较简单，就不过多介绍了。值得留意的是基本类型指针的切片，即上面的PtrStringSlice字段，类型为[]*string。 由于结构中存储的是字符串指针，go-flags在解析过程中遇到该选项会自动创建字符串，将指针追加到切片中。\n运行程序，传入--pstrslice选项：\n1 2 3 4 $ ./main.exe --pstrslice test1 --pstrslice test2 slice of pointer of string flag: 0: test1 1: test2 另外，我们可以在选项中定义函数类型。该函数的唯一要求是有一个字符串类型的参数。解析中每次遇到该选项就会以选项值为参数调用这个函数。 上面代码中，Call函数只是简单的打印传入的选项值。运行代码，传入--call选项：\n1 2 3 $ ./main.exe --call test1 --call test2 in callback: test1 in callback: test2 最后，go-flags还支持 map 类型。虽然限制键必须是string类型，值必须是基本类型，也能实现比较灵活的配置。 map类型的选项值中键-值通过:分隔，如key:value，可设置多个。运行代码，传入--intmap选项：\n1 2 $ ./main.exe --intmap key1:12 --intmap key2:58 int map: map[key1:12 key2:58] 常用设置 go-flags提供了非常多的设置选项，具体可参见文档。这里重点介绍两个required和default。\nrequired非空时，表示对应的选项必须设置值，否则解析时返回ErrRequired错误。\ndefault用于设置选项的默认值。如果已经设置了默认值，那么required是否设置并不影响，也就是说命令行参数中该选项可以没有。\n看下面示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/jessevdk/go-flags\u0026#34; ) type Option struct { Required string `short:\u0026#34;r\u0026#34; long:\u0026#34;required\u0026#34; required:\u0026#34;true\u0026#34;` Default string `short:\u0026#34;d\u0026#34; long:\u0026#34;default\u0026#34; default:\u0026#34;default\u0026#34;` } func main() { var opt Option _, err := flags.Parse(\u0026amp;opt) if err != nil { log.Fatal(\u0026#34;Parse error:\u0026#34;, err) } fmt.Println(\u0026#34;required: \u0026#34;, opt.Required) fmt.Println(\u0026#34;default: \u0026#34;, opt.Default) } 运行程序，不传入default选项，Default字段取默认值，不传入required选项，执行报错：\n1 2 3 4 5 6 7 8 9 10 11 $ ./main.exe -r required-data required: required-data default: default $ ./main.exe -d default-data -r required-data required: required-data default: default-data $ ./main.exe the required flag `/r, /required\u0026#39; was not specified 2020/01/09 18:07:39 Parse error:the required flag `/r, /required\u0026#39; was not specified 高级特性 选项分组 1 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 package main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/jessevdk/go-flags\u0026#34; ) type Option struct { GroupBasicOption `group:\u0026#34;basic\u0026#34;` GroupSliceOption `group:\u0026#34;slice\u0026#34;` } type GroupBasicOption struct { IntFlag int `short:\u0026#34;i\u0026#34; long:\u0026#34;intflag\u0026#34; description:\u0026#34;int flag\u0026#34;` BoolFlag bool `short:\u0026#34;b\u0026#34; long:\u0026#34;boolflag\u0026#34; description:\u0026#34;bool flag\u0026#34;` FloatFlag float64 `short:\u0026#34;f\u0026#34; long:\u0026#34;floatflag\u0026#34; description:\u0026#34;float flag\u0026#34;` StringFlag string `short:\u0026#34;s\u0026#34; long:\u0026#34;stringflag\u0026#34; description:\u0026#34;string flag\u0026#34;` } type GroupSliceOption struct { IntSlice int `long:\u0026#34;intslice\u0026#34; description:\u0026#34;int slice\u0026#34;` BoolSlice bool `long:\u0026#34;boolslice\u0026#34; description:\u0026#34;bool slice\u0026#34;` FloatSlice float64 `long:\u0026#34;floatslice\u0026#34; description:\u0026#34;float slice\u0026#34;` StringSlice string `long:\u0026#34;stringslice\u0026#34; description:\u0026#34;string slice\u0026#34;` } func main() { var opt Option p := flags.NewParser(\u0026amp;opt, flags.Default) _, err := p.Parse() if err != nil { var errPtr *flags.Error if errors.As(err, \u0026amp;errPtr) { return } log.Fatal(err) } fmt.Println(opt) } 上面代码中我们将基本类型和它们的切片类型选项拆分到两个结构体中，这样可以使代码看起来更清晰自然，特别是在代码量很大的情况下。 这样做还有一个好处，我们试试用--help运行该程序：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ go run main.go --help Usage: C:\\Users\\Lenovo\\AppData\\Local\\Temp\\go-build4141518920\\b001\\exe\\main.exe [OPTIONS] basic: /i, /intflag: int flag /b, /boolflag bool flag /f, /floatflag: float flag /s, /stringflag: string flag slice: /intslice: int slice /boolslice bool slice /floatslice: float slice /stringslice: string slice Help Options: /? Show this help message /h, /help Show this help message 输出的帮助信息中，也是按照我们设定的分组显示了，便于查看。\ngroup标签字段用于分组并命名。当在结构体字段上指定时，使该结构体字段具有给定名称的单独组（可选）\n子命令 go-flags支持子命令。我们经常使用的 Go 和 Git 命令行程序就有大量的子命令。例如go version、go build、go run、git status、git commit这些命令中version/build/run/status/commit就是子命令。 使用go-flags定义子命令比较简单：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 package main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;github.com/jessevdk/go-flags\u0026#34; ) type Option struct { GroupBasicOption `group:\u0026#34;Basic\u0026#34;` GroupSliceOption `group:\u0026#34;Slice\u0026#34;` MathCommand `command:\u0026#34;math\u0026#34;` Call func(int) `long:\u0026#34;call\u0026#34; description:\u0026#34;callback\u0026#34;` } type GroupBasicOption struct { IntFlag int `short:\u0026#34;i\u0026#34; long:\u0026#34;intflag\u0026#34; description:\u0026#34;int flag\u0026#34;` BoolFlag bool `short:\u0026#34;b\u0026#34; long:\u0026#34;boolflag\u0026#34; description:\u0026#34;bool flag\u0026#34;` FloatFlag float64 `short:\u0026#34;f\u0026#34; long:\u0026#34;floatflag\u0026#34; description:\u0026#34;float flag\u0026#34;` StringFlag string `short:\u0026#34;s\u0026#34; long:\u0026#34;stringflag\u0026#34; description:\u0026#34;string flag\u0026#34;` } type GroupSliceOption struct { IntSlice int `long:\u0026#34;intslice\u0026#34; description:\u0026#34;int slice\u0026#34;` BoolSlice bool `long:\u0026#34;boolslice\u0026#34; description:\u0026#34;bool slice\u0026#34;` FloatSlice float64 `long:\u0026#34;floatslice\u0026#34; description:\u0026#34;float slice\u0026#34;` StringSlice string `long:\u0026#34;stringslice\u0026#34; description:\u0026#34;string slice\u0026#34;` } type MathCommand struct { Op string `long:\u0026#34;op\u0026#34; description:\u0026#34;operation to execute\u0026#34;` Args []string Result int64 } func (m *MathCommand) Execute(args []string) error { // 注意，不能使用乘法符号*和除法符号/，它们都不可识别。 if m.Op != \u0026#34;+\u0026#34; \u0026amp;\u0026amp; m.Op != \u0026#34;-\u0026#34; \u0026amp;\u0026amp; m.Op != \u0026#34;*\u0026#34; \u0026amp;\u0026amp; m.Op != \u0026#34;/\u0026#34; { return errors.New(\u0026#34;invalid op\u0026#34;) } for _, arg := range args { num, err := strconv.ParseInt(arg, 10, 64) if err != nil { return err } // 只实现了加的功能 m.Result += num } m.Args = args return nil } func main() { var opt Option opt.Call = func(i int) { fmt.Println(i) } //_, errParse := flags.Parse(\u0026amp;opt) _, errParse := flags.NewParser(\u0026amp;opt, flags.HelpFlag|flags.PassDoubleDash).Parse() if errParse != nil { var p *flags.Error if errors.As(errParse, \u0026amp;p) { /*if errors.Is(p.Type, flags.ErrHelp) { os.Exit(0) }*/ switch { case errors.Is(p.Type, flags.ErrHelp): fmt.Println(p.Message) os.Exit(0) case errors.Is(p.Type, flags.ErrCommandRequired): default: log.Fatal(p.Message) } } else { log.Fatal(errParse) } } fmt.Println(opt) fmt.Printf(\u0026#34;The result of %s is %d\u0026#34;, strings.Join(opt.MathCommand.Args, opt.MathCommand.Op), opt.MathCommand.Result) } 子命令必须实现go-flags定义的Commander接口：\n1 2 3 type Commander interface { Execute(args []string) error } command结构体标签字段：当在结构体字段上指定时，使该结构体字段具有给定名称的（子）命令\n解析命令行时，如果遇到不是以-或--开头的参数，go-flags会尝试将其解释为子命令名。子命令的名字通过在结构标签中使用command指定。 子命令后面的参数都将作为子命令的参数，子命令也可以有选项。\n上面代码中，我们实现了一个可以计算任意个整数的加、减、乘、除子命令math。\n接下来看看如何使用：\n1 2 3 $ go run main.go math --op + 1 2 3 {{0 false 0 } {0 false 0 } {+ [1 2 3] 6} 0x6809a0} The result of 1+2+3 is 6 注意，不能使用乘法符号*和除法符号/，它们都不可识别。\n其他 go-flags库还有很多有意思的特性，例如支持 Windows 选项格式（/v和/verbose）、从环境变量中读取默认值、从 ini 文件中读取默认设置等等。大家有兴趣可以自行去研究~\n参考 go-flagsGoDoc 文档 Go 每日一库之 go-flags ","date":"2024-05-01T21:50:22+08:00","permalink":"https://arlettebrook.github.io/p/go%E8%A7%A3%E6%9E%90%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0%E4%B9%8B%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93go-flags/","title":"Go解析命令行参数之第三方库go-flags"},{"content":" zap 是由 Uber 公司开源的一款 Go 日志库，就像它的命名一样，zap 以快著称。官方 GitHub 仓库中只用一句话来概括 zap：「在 Go 中进行快速、结构化、分级的日志记录」。这句话简单明了的概括了 zap 的核心特性，今天我们就来介绍下 zap 日志库的基本使用和高级特性，以及如何在实际应用程序中使用，来提高应用程序的可靠性。\n特点 zap 具有如下特点：\n快，非常快，这也是 zap 最显著的特点。速度快的原因是 zap 避免使用 interface{} 和反射，并且使用 sync.Pool 减少堆内存分配。在 zap 面前 Logrus 的执行速度只有被吊打的份，你可以在官方 GitHub 仓库中看到 zap 与不同日志库的速度对比。 支持结构化日志记录。这是一个优秀的日志库必备功能。 支持七种日志级别：Debug、Info、Warn、Error、DPanic、Panic、Fatal，其中 DPanic 是指在开发环境下（development）记录日志后会进行 panic。 支持输出调用堆栈。 支持 Hooks 机制。 使用 基本使用 先安装go get -u go.uber.org/zap\n基本使用如下：\nzap库的使用与其他的日志库非常相似。先创建一个logger，然后调用各个级别的方法记录日志（Debug/Info/Warn/Error/）。zap提供了几个快速创建logger的方法，zap.NewExample()、zap.NewDevelopment()、zap.NewProduction()，还有高度定制化的创建方法zap.New()。创建前 3 个logger时，zap会使用一些预定义的设置，它们的使用场景也有所不同。Example适合用在测试代码中，Development在开发环境中使用，Production用在生成环境。\nzap底层 API 可以设置缓存，所以一般使用defer logger.Sync()将缓存同步到文件中。刷新缓存，确保日志输出。\n由于fmt.Printf之类的方法大量使用interface{}和反射，会有不少性能损失，并且增加了内存分配的频次。zap为了提高性能、减少内存分配次数，没有使用反射，而且默认的Logger只支持强类型的、结构化的日志。必须使用zap提供的方法记录字段。zap为 Go 语言中所有的基本类型和其他常见类型都提供了方法。这些方法的名称也比较好记忆，zap.Type（Type为bool/int/uint/float64/complex64/time.Time/time.Duration/error等）就表示该类型的字段，zap.Typep以p结尾表示该类型指针的字段，zap.Types以s结尾表示该类型切片的字段。如：\nzap.Bool(key string, val bool) Field：bool字段 zap.Boolp(key string, val *bool) Field：bool指针字段； zap.Bools(key string, val []bool) Field：bool切片字段。 当然也有一些特殊类型的字段：\nzap.Any(key string, value interface{}) Field：任意类型的字段； zap.Binary(key string, val []byte) Field：二进制串的字段。 当然，每个字段都用方法包一层用起来比较繁琐。zap也提供了便捷的方法SugarLogger，可以使用printf格式符的方式。调用logger.Sugar()即可创建SugaredLogger。SugaredLogger的使用比Logger简单，只是性能比Logger低 50% 左右，可以用在非热点函数中。调用SugarLogger以f结尾的方法与fmt.Printf没什么区别，如例子中的Infof。同时SugarLogger还支持以w结尾的方法，这种方式不需要先创建字段对象，直接将字段名和值依次放在参数中即可，如例子中的Infow。\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; \u0026#34;go.uber.org/zap\u0026#34; ) func main() { // 生产环境 fmt.Println(\u0026#34;------生产环境------\u0026#34;) { logger, _ := zap.NewProduction() defer logger.Sync() // 刷新 buffer，保证日志最终会被输出 url := \u0026#34;https://arlettebrook.github.io/\u0026#34; logger.Info(\u0026#34;production failed to fetch URL\u0026#34;, zap.String(\u0026#34;url\u0026#34;, url), // 因为没有使用 interface{} 和反射机制，所以需要指定具体类型 zap.Int(\u0026#34;attempt\u0026#34;, 3), zap.Duration(\u0026#34;backoff\u0026#34;, time.Second), ) } // 开发环境 fmt.Println(\u0026#34;------开发环境------\u0026#34;) { logger, _ := zap.NewDevelopment() defer logger.Sync() url := \u0026#34;https://arlettebrook.github.io/\u0026#34; logger.Debug(\u0026#34;development failed to fetch URL\u0026#34;, zap.String(\u0026#34;url\u0026#34;, url), zap.Int(\u0026#34;attempt\u0026#34;, 3), zap.Duration(\u0026#34;backoff\u0026#34;, time.Second), ) } // 测试环境 fmt.Println(\u0026#34;------测试环境------\u0026#34;) { logger := zap.NewExample() defer logger.Sync() url := \u0026#34;https://arlettebrook.github.io/\u0026#34; logger.Info(\u0026#34;failed to fetch URL\u0026#34;, zap.String(\u0026#34;url\u0026#34;, url), zap.Int(\u0026#34;attempt\u0026#34;, 3), zap.Duration(\u0026#34;backoff\u0026#34;, time.Second), ) fmt.Println(\u0026#34;------sugaredLogger------\u0026#34;) sugar := logger.Sugar() sugar.Infow(\u0026#34;failed to fetch URL\u0026#34;, \u0026#34;url\u0026#34;, url, \u0026#34;attempt\u0026#34;, 3, \u0026#34;backoff\u0026#34;, time.Second, ) sugar.Infof(\u0026#34;Failed to fetch URL: %s\u0026#34;, url) } } zap 针对生产环境、开发环境以及测试环境提供了不同的函数来创建 Logger 对象。\n如果想在日志后面追加 key-value，则需要根据 value 的数据类型使用 zap.String、zap.Int 等方法实现。这一点在使用上显然不如 Logrus 等其他日志库来的方便，但这也是 zap 速度快的原因之一，zap 内部尽量避免使用 interface{} 和反射来提高代码执行效率。\n记录日志的 logger.Xxx 方法签名如下：\n1 func (log *Logger) Info(msg string, fields ...Field) 其中 fields 是 zapcore.Field 类型，用来存储 key-value，并记录 value 类型，不管是 zap.String 还是 zap.Int 底层都是 zapcore.Field 类型来记录的。zap 为每一种 Go 的内置类型都定义了对应的 zap.Xxx 方法，甚至还实现 zap.Any() 来支持 interface{}。\n执行以上代码，控制台得到如下输出：\n1 2 3 4 5 6 7 8 9 10 11 12 13 ------生产环境------ {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1714375211.8196504,\u0026#34;caller\u0026#34;:\u0026#34;learn/main.go:18\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;production failed to fe tch URL\u0026#34;,\u0026#34;url\u0026#34;:\u0026#34;https://arlettebrook.github.io/\u0026#34;,\u0026#34;attempt\u0026#34;:3,\u0026#34;backoff\u0026#34;:1} ------开发环境------ 2024-04-29T15:20:11.820+0800 DEBUG learn/main.go:32 development failed to fetch URL {\u0026#34; url\u0026#34;: \u0026#34;https://arlettebrook.github.io/\u0026#34;, \u0026#34;attempt\u0026#34;: 3, \u0026#34;backoff\u0026#34;: \u0026#34;1s\u0026#34;} ------测试环境------ {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;failed to fetch URL\u0026#34;,\u0026#34;url\u0026#34;:\u0026#34;https://arlettebrook.github.io/\u0026#34;,\u0026#34;attempt\u0026#34;:3,\u0026#34;b ackoff\u0026#34;:\u0026#34;1s\u0026#34;} ------sugaredLogger------ {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;failed to fetch URL\u0026#34;,\u0026#34;url\u0026#34;:\u0026#34;https://arlettebrook.github.io/\u0026#34;,\u0026#34;attempt\u0026#34;:3,\u0026#34;b ackoff\u0026#34;:\u0026#34;1s\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;Failed to fetch URL: https://arlettebrook.github.io/\u0026#34;} 可以发现，通过 zap.NewProduction() 创建的日志对象输出格式为 JSON，而通过 zap.NewDevelopment() 创建的日志对象输出格式为 Text，日志后面追加的 key-value 会被转换成 JSON。并且，两者输出的字段内容也略有差异，如生产环境日志输出的时间格式为 Unix epoch 利于程序解析，而开发环境日志输出的时间格式为 ISO8601 更利于人类阅读。测试环境没有文件行号、堆栈跟踪信息以及时间，格式为JSON。对应的sugar都是是一样的。\n导致以上这些差异的原因是配置不同，我们来看下 zap.NewProduction 和 zap.NewDevelopment 的代码实现：\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 func NewProduction(options ...Option) (*Logger, error) { return NewProductionConfig().Build(options...) } func NewProductionConfig() Config { return Config{ Level: NewAtomicLevelAt(InfoLevel), Development: false, Sampling: \u0026amp;SamplingConfig{ Initial: 100, Thereafter: 100, }, Encoding: \u0026#34;json\u0026#34;, EncoderConfig: NewProductionEncoderConfig(), OutputPaths: []string{\u0026#34;stderr\u0026#34;}, ErrorOutputPaths: []string{\u0026#34;stderr\u0026#34;}, } } func NewDevelopment(options ...Option) (*Logger, error) { return NewDevelopmentConfig().Build(options...) } func NewDevelopmentConfig() Config { return Config{ Level: NewAtomicLevelAt(DebugLevel), Development: true, Encoding: \u0026#34;console\u0026#34;, EncoderConfig: NewDevelopmentEncoderConfig(), OutputPaths: []string{\u0026#34;stderr\u0026#34;}, ErrorOutputPaths: []string{\u0026#34;stderr\u0026#34;}, } } 可以看到，两者在实现思路上是一样的，都是先创建一个配置对象 zap.Config，然后再调用配置对象的 Build 方法来构建 Logger。\nzap.Config 定义如下：\n1 2 3 4 5 6 7 8 9 10 11 12 type Config struct { Level AtomicLevel `json:\u0026#34;level\u0026#34; yaml:\u0026#34;level\u0026#34;` Development bool `json:\u0026#34;development\u0026#34; yaml:\u0026#34;development\u0026#34;` DisableCaller bool `json:\u0026#34;disableCaller\u0026#34; yaml:\u0026#34;disableCaller\u0026#34;` DisableStacktrace bool `json:\u0026#34;disableStacktrace\u0026#34; yaml:\u0026#34;disableStacktrace\u0026#34;` Sampling *SamplingConfig `json:\u0026#34;sampling\u0026#34; yaml:\u0026#34;sampling\u0026#34;` Encoding string `json:\u0026#34;encoding\u0026#34; yaml:\u0026#34;encoding\u0026#34;` EncoderConfig zapcore.EncoderConfig `json:\u0026#34;encoderConfig\u0026#34; yaml:\u0026#34;encoderConfig\u0026#34;` OutputPaths []string `json:\u0026#34;outputPaths\u0026#34; yaml:\u0026#34;outputPaths\u0026#34;` ErrorOutputPaths []string `json:\u0026#34;errorOutputPaths\u0026#34; yaml:\u0026#34;errorOutputPaths\u0026#34;` InitialFields map[string]interface{} `json:\u0026#34;initialFields\u0026#34; yaml:\u0026#34;initialFields\u0026#34;` } 每个配置项说明如下：\nLevel: 日志级别。 Development: 是否为开发模式。 DisableCaller: 禁用调用信息，值为 true 时，日志中将不再显示记录日志时所在的函数调用文件名和行号。 DisableStacktrace: 禁用堆栈跟踪捕获。 Sampling: 采样策略配置，单位为每秒，作用是限制日志在每秒内的输出数量，以此来防止全局的 CPU 和 I/O 负载过高。 Encoding: 指定日志编码器，目前支持 json 和 console。 EncoderConfig: 编码配置，决定了日志字段格式。 OutputPaths: 配置日志输出位置，URLs 或文件路径，可配置多个。 ErrorOutputPaths: zap 包内部出现错误的日志输出位置，URLs 或文件路径，可配置多个，默认 os.Stderr。 InitialFields: 初始化字段配置，该配置的字段会以结构化的形式打印在每条日志输出中。 我们再来对比下 NewProductionEncoderConfig() 和 NewDevelopmentEncoderConfig() 这两个配置的不同：\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 func NewProductionEncoderConfig() zapcore.EncoderConfig { return zapcore.EncoderConfig{ TimeKey: \u0026#34;ts\u0026#34;, LevelKey: \u0026#34;level\u0026#34;, NameKey: \u0026#34;logger\u0026#34;, CallerKey: \u0026#34;caller\u0026#34;, FunctionKey: zapcore.OmitKey, MessageKey: \u0026#34;msg\u0026#34;, StacktraceKey: \u0026#34;stacktrace\u0026#34;, LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.LowercaseLevelEncoder, EncodeTime: zapcore.EpochTimeEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, } } func NewDevelopmentEncoderConfig() zapcore.EncoderConfig { return zapcore.EncoderConfig{ // Keys can be anything except the empty string. TimeKey: \u0026#34;T\u0026#34;, LevelKey: \u0026#34;L\u0026#34;, NameKey: \u0026#34;N\u0026#34;, CallerKey: \u0026#34;C\u0026#34;, FunctionKey: zapcore.OmitKey, MessageKey: \u0026#34;M\u0026#34;, StacktraceKey: \u0026#34;S\u0026#34;, LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.CapitalLevelEncoder, EncodeTime: zapcore.ISO8601TimeEncoder, EncodeDuration: zapcore.StringDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, } } 对比来看，两者有很多不同的配置，比如生产环境下 EncodeTime 值为 zapcore.EpochTimeEncoder，开发环境下 EncodeTime 值为 zapcore.ISO8601TimeEncoder。这就是生产环境日志输出的时间格式为 Unix epoch 而开发环境日志输出的时间格式为 ISO8601 的原因。\nzapcore.EncoderConfig 其他几个常用的配置项说明如下：\nMessageKey: 日志信息的键名，默认 msg。 LevelKey: 日志级别的键名，默认 level。 TimeKey: 日志时间的键名。 EncodeLevel: 日志级别的格式，默认为小写，如 info。 除了提供 zap.NewProduction() 和 zap.NewDevelopment() 两个构造函数外，zap 还提供了 zap.NewExample() 来创建一个 Logger 对象，这个方法主要用于测试，这里就不多介绍了。\n记录层级关系 前面我们记录的日志都是一层结构，没有嵌套的层级。我们可以使用zap.Namespace(key string) Field构建一个命名空间，后续的Field都记录在此命名空间中：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main() { logger := zap.NewExample() defer logger.Sync() logger.Info(\u0026#34;tracked some metrics\u0026#34;, zap.Namespace(\u0026#34;metrics\u0026#34;), zap.Int(\u0026#34;counter\u0026#34;, 1), ) logger2 := logger.With( zap.Namespace(\u0026#34;metrics\u0026#34;), zap.Int(\u0026#34;counter\u0026#34;, 1), ) logger2.Info(\u0026#34;tracked some metrics\u0026#34;) } 输出：\n1 2 {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;tracked some metrics\u0026#34;,\u0026#34;metrics\u0026#34;:{\u0026#34;counter\u0026#34;:1}} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;tracked some metrices\u0026#34;,\u0026#34;metrics\u0026#34;:{\u0026#34;counter\u0026#34;:1}} 上面我们演示了两种Namespace的用法，一种是直接作为字段传入Debug/Info等方法，一种是调用With()创建一个新的Logger，新的Logger记录日志时总是带上预设的字段。With()方法实际上是创建了一个新的Logger：\n1 2 3 4 5 6 7 8 9 // src/go.uber.org/zap/logger.go func (log *Logger) With(fields ...Field) *Logger { if len(fields) == 0 { return log } l := log.clone() l.core = l.core.With(fields) return l } 预设日志字段 如果每条日志都要记录一些共用的字段，那么使用zap.Fields(fs ...Field)创建的选项。例如在服务器日志中记录可能都需要记录serverId和serverName：\n1 2 3 4 5 6 7 8 func main() { logger := zap.NewExample(zap.Fields( zap.Int(\u0026#34;serverId\u0026#34;, 90), zap.String(\u0026#34;serverName\u0026#34;, \u0026#34;awesome web\u0026#34;), )) logger.Info(\u0026#34;hello world\u0026#34;) } 输出：\n1 {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;hello world\u0026#34;,\u0026#34;serverId\u0026#34;:90,\u0026#34;serverName\u0026#34;:\u0026#34;awesome web\u0026#34;} 与logger.with()差不多\n给语法加点糖 zap 虽然速度足够快，但是多数情况下，我们并不需要极致的性能，而是想让代码写起来更爽一些。zap 为我们提供了解决方案 —— SugaredLogger。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package main import ( \u0026#34;time\u0026#34; \u0026#34;go.uber.org/zap\u0026#34; ) func main() { logger, _ := zap.NewProduction() defer logger.Sync() url := \u0026#34;https://arlettebrook.github.io/\u0026#34; sugar := logger.Sugar() sugar.Infow(\u0026#34;production failed to fetch URL\u0026#34;, \u0026#34;url\u0026#34;, url, \u0026#34;attempt\u0026#34;, 3, \u0026#34;backoff\u0026#34;, time.Second, ) sugar.Info(\u0026#34;Info\u0026#34;) sugar.Infoln(\u0026#34;Infoln\u0026#34;) sugar.Infof(\u0026#34;Infof: %s\u0026#34;, url) } 通过 logger.Sugar() 方法可以将一个 Logger 对象转换成一个 SugaredLogger 对象。\nSugaredLogger 提供了更人性化的接口，日志中追加 key-value 时不在需要 zap.String(\u0026quot;url\u0026quot;, url) 这种显式指明类型的写法，只需要保证 key 为 string 类型，value 则可以为任意类型，能够减少我们编写的代码量。\n此外，为了满足不同需求，SugaredLogger 提供了四种方式输出日志：sugar.Xxx、sugar.Xxxw、sugar.Xxxf、sugar.Xxxln。\n执行以上代码，控制台得到如下输出：\n1 2 3 4 5 6 {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1714398451.8505704,\u0026#34;caller\u0026#34;:\u0026#34;learn/main.go:15\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;production failed to fe tch URL\u0026#34;,\u0026#34;url\u0026#34;:\u0026#34;https://arlettebrook.github.io/\u0026#34;,\u0026#34;attempt\u0026#34;:3,\u0026#34;backoff\u0026#34;:1} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1714398451.8511178,\u0026#34;caller\u0026#34;:\u0026#34;learn/main.go:20\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;Info\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1714398451.851623,\u0026#34;caller\u0026#34;:\u0026#34;learn/main.go:21\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;Infoln\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1714398451.8516397,\u0026#34;caller\u0026#34;:\u0026#34;learn/main.go:22\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;Infof: https://arletteb rook.github.io/\u0026#34;} 我们知道，这种方便的写法是有一定代价的，所以开发中是否需要使用 SugaredLogger 来记录日志，需要根据程序的特点来决定。SugaredLogger 与 Logger 的性能对比同样可以在官方 GitHub 仓库中看到。\n定制 Logger 通过查看 zap.NewProduction() 和 zap.NewDevelopment() 两个构造函数源码，我们知道可以使用 zap.Config 对象的 Build 方法创建 Logger 对象。那么我们很容易能够想到，如果要定制 Logger，只需要创建一个定制的 zap.Config 即可。\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 package main import ( \u0026#34;go.uber.org/zap\u0026#34; \u0026#34;go.uber.org/zap/zapcore\u0026#34; ) func newCustomLogger() (*zap.Logger, error) { cfg := zap.Config{ Level: zap.NewAtomicLevelAt(zap.DebugLevel), Development: false, Encoding: \u0026#34;json\u0026#34;, EncoderConfig: zapcore.EncoderConfig{ TimeKey: \u0026#34;time\u0026#34;, LevelKey: \u0026#34;level\u0026#34;, NameKey: \u0026#34;logger\u0026#34;, CallerKey: \u0026#34;\u0026#34;, // 不记录日志调用位置 FunctionKey: zapcore.OmitKey, MessageKey: \u0026#34;message\u0026#34;, LineEnding: zapcore.DefaultLineEnding, EncodeLevel: zapcore.LowercaseLevelEncoder, EncodeTime: zapcore.RFC3339TimeEncoder, EncodeDuration: zapcore.SecondsDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, }, OutputPaths: []string{\u0026#34;stdout\u0026#34;, \u0026#34;test.log\u0026#34;}, ErrorOutputPaths: []string{\u0026#34;error.log\u0026#34;}, } return cfg.Build() } func main() { logger, _ := newCustomLogger() defer logger.Sync() // 增加一个 skip 选项，触发 zap 内部 error，将错误输出到 error.log logger = logger.WithOptions(zap.AddCallerSkip(100)) logger.Info(\u0026#34;Info msg\u0026#34;) logger.Error(\u0026#34;Error msg\u0026#34;) } 以上代码通过 newCustomLogger 函数创建了一个自定义的 Logger，同样通过先定义一个 zap.Config 然后再调用其 Build 方法来实现。\n配置日志分别输出到标准输出和 test.log 文件，执行以上代码，控制台和 test.log 都会得到如下输出：\n1 2 {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;time\u0026#34;:\u0026#34;2023-03-19T19:19:18+08:00\u0026#34;,\u0026#34;message\u0026#34;:\u0026#34;Info msg\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;time\u0026#34;:\u0026#34;2023-03-19T19:19:18+08:00\u0026#34;,\u0026#34;message\u0026#34;:\u0026#34;Error msg\u0026#34;} 另外，我们还通过 logger.WithOptions() 为 Logger 对象增加了一个选项 zap.AddCallerSkip(100)，这个选项的作用是指定在通过调用栈获得行号时跳过的调用深度，因为我们的函数调用栈并不是 100 层，所以会触发 zap 内部错误，zap 会将错误日志输出到 ErrorOutputPaths 配置指定的位置中，即 error.log。\nerror.log 得到的错误日志如下：\n1 2 2023-03-19 11:19:18.438824 +0000 UTC Logger.check error: failed to get caller 2023-03-19 11:19:18.44921 +0000 UTC Logger.check error: failed to get caller 选项 logger.WithOptions() 支持的选项如下：\nWrapCore(f func(zapcore.Core) zapcore.Core): 使用一个新的 zapcore.Core 替换掉 Logger 内部原有的的 zapcore.Core 属性。 Hooks(hooks ...func(zapcore.Entry) error): 注册钩子函数，用来在日志打印时同时调用注册的钩子函数。 Fields(fs ...Field): 添加公共字段。 ErrorOutput(w zapcore.WriteSyncer): 指定日志组件内部出现异常时的输出位置。 Development(): 将日志记录器设为开发模式，这将使 DPanic 级别日志记录错误后执行 panic()。 AddCaller(): 与 WithCaller(true) 等价。 WithCaller(enabled bool): 指定是否在日志输出内容中增加调用信息，即文件名和行号。 AddCallerSkip(skip int): 指定在通过调用栈获取文件名和行号时跳过的调用深度。 AddStacktrace(lvl zapcore.LevelEnabler): 用来指定某个日志级别及以上级别输出调用堆栈。 IncreaseLevel(lvl zapcore.LevelEnabler): 提高日志级别，如果传入的 lvl 比现有级别低，则不会改变日志级别。 WithFatalHook(hook zapcore.CheckWriteHook): 当出现 Fatal 级别日志时调用的钩子函数。 WithClock(clock zapcore.Clock): 指定日志记录器用来确定当前时间的 zapcore.Clock 对象，默认为 time.Now 的系统时钟。 NewExample()/NewDevelopment()/NewProduction()这 3 个函数可以传入若干类型为zap.Option的选项，从而定制Logger的行为。又一次见到了选项模式！！\nzap提供了丰富的选项供我们选择。\n输出文件名和行号\n调用zap.AddCaller()返回的选项设置输出文件名和行号。但是有一个前提，必须设置配置对象Config中的CallerKey字段。也因此NewExample()不能输出这个信息（它的Config没有设置CallerKey）。\nAddCaller()与zap.WithCaller(true)等价。一般不用\n有时我们稍微封装了一下记录日志的方法，但是我们希望输出的文件名和行号是调用封装函数的位置。这时可以使用zap.AddCallerSkip(skip int)向上跳 1 层。可能会用到。\n输出调用堆栈\n有时候在某个函数处理中遇到了异常情况，因为这个函数可能在很多地方被调用。如果我们能输出此次调用的堆栈，那么分析起来就会很方便。我们可以使用zap.AddStackTrace(lvl zapcore.LevelEnabler)达成这个目的。该函数指定lvl和之上的级别都需要输出调用堆栈：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package main import ( \u0026#34;go.uber.org/zap\u0026#34; \u0026#34;go.uber.org/zap/zapcore\u0026#34; ) func f1() { f2(\u0026#34;hello world\u0026#34;) } func f2(msg string, fields ...zap.Field) { zap.L().Warn(msg, fields...) // zap.L()获取全局logger } func main() { logger, _ := zap.NewDevelopment(zap.AddStacktrace(zapcore.WarnLevel)) defer logger.Sync() zap.ReplaceGlobals(logger) // 替换全局logger f1() } 将zapcore.WarnLevel传入AddStacktrace()，之后Warn()/Error()等级别的日志会输出堆栈，Debug()/Info()这些级别不会。运行结果：\n1 2 3 4 5 6 7 8 9 2024-04-30T10:32:49.798+0800 WARN learn/main.go:13 hello world main.f2 F:/GoProject/learn/main.go:13 main.f1 F:/GoProject/learn/main.go:9 main.main F:/GoProject/learn/main.go:22 runtime.main D:/Go/src/runtime/proc.go:271 很清楚地看到调用路径。\n创建自定义的配置对象，除了在代码中指定配置参数，也可以将这些配置项写入到 JSON 文件中，然后通过 json.Unmarshal 的方式将配置绑定到 zap.Config，可以参考官方示例。\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 func main() { rawJSON := []byte(`{ \u0026#34;level\u0026#34;:\u0026#34;debug\u0026#34;, \u0026#34;encoding\u0026#34;:\u0026#34;json\u0026#34;, \u0026#34;outputPaths\u0026#34;: [\u0026#34;stdout\u0026#34;, \u0026#34;server.log\u0026#34;], \u0026#34;errorOutputPaths\u0026#34;: [\u0026#34;stderr\u0026#34;], \u0026#34;initialFields\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;dj\u0026#34;}, \u0026#34;encoderConfig\u0026#34;: { \u0026#34;messageKey\u0026#34;: \u0026#34;message\u0026#34;, \u0026#34;levelKey\u0026#34;: \u0026#34;level\u0026#34;, \u0026#34;levelEncoder\u0026#34;: \u0026#34;lowercase\u0026#34; } }`) var cfg zap.Config if err := json.Unmarshal(rawJSON, \u0026amp;cfg); err != nil { panic(err) } logger, err := cfg.Build() if err != nil { panic(err) } defer logger.Sync() logger.Info(\u0026#34;server start work successfully!\u0026#34;) } 上面创建一个输出到标准输出stdout和文件server.log的Logger。观察输出：\n1 {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;message\u0026#34;:\u0026#34;server start work successfully!\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;dj\u0026#34;} 全局Logger 为了方便使用，zap提供了两个全局的Logger，一个是*zap.Logger，可调用zap.L()获得；另一个是*zap.SugaredLogger，可调用zap.S()获得。需要注意的是，全局的Logger默认并不会记录日志！它是一个无实际效果的Logger。看源码:\n1 2 3 4 5 6 // go.uber.org/zap/global.go var ( _globalMu sync.RWMutex _globalL = NewNop() _globalS = _globalL.Sugar() ) 我们可以使用ReplaceGlobals(logger *Logger) func()将logger设置为全局的Logger，该函数返回一个无参函数，用于恢复全局Logger设置：\n1 2 3 4 5 6 7 8 9 10 11 func main() { zap.L().Info(\u0026#34;global Logger before\u0026#34;) zap.S().Info(\u0026#34;global SugaredLogger before\u0026#34;) logger := zap.NewExample() defer logger.Sync() zap.ReplaceGlobals(logger) zap.L().Info(\u0026#34;global Logger after\u0026#34;) zap.S().Info(\u0026#34;global SugaredLogger after\u0026#34;) } 输出：\n1 2 {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;global Logger after\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;global SugaredLogger after\u0026#34;} 可以看到在调用ReplaceGlobals之前记录的日志并没有输出。\n与标准日志库搭配使用 如果项目一开始使用的是标准日志库log，后面想转为zap。这时不必修改每一个文件。我们可以调用zap.NewStdLog(l *Logger) *log.Logger返回一个标准的log.Logger，内部实际上写入的还是我们之前创建的zap.Logger：\n1 2 3 4 5 6 7 func main() { logger := zap.NewExample() defer logger.Sync() std := zap.NewStdLog(logger) std.Print(\u0026#34;standard logger wrapper\u0026#34;) } 输出：\n1 {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;standard logger wrapper\u0026#34;} 很方便不是吗？我们还可以使用NewStdLogAt(l *logger, level zapcore.Level) (*log.Logger, error)让标准接口以level级别写入内部的*zap.Logger。\n如果我们只是想在一段代码内使用标准日志库log，其它地方还是使用zap.Logger。可以调用RedirectStdLog(l *Logger) func()。它会返回一个无参函数恢复设置：\n1 2 3 4 5 6 7 8 9 10 func main() { logger := zap.NewExample() defer logger.Sync() undo := zap.RedirectStdLog(logger) log.Print(\u0026#34;redirected standard library\u0026#34;) undo() log.Print(\u0026#34;restored standard library\u0026#34;) } 看前后输出变化：\n1 2 {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;redirected standard library\u0026#34;} 2020/04/24 22:13:58 restored standard library 当然RedirectStdLog也有一个对应的RedirectStdLogAt以特定的级别调用内部的*zap.Logger方法。\n参考 Go 每日一库之 zap zap 源码: https://github.com/uber-go/zap zap 文档: https://pkg.go.dev/go.uber.org/zap Go 第三方 log 库之 zap 使用 如何基于 zap 封装一个更好用的日志库 ","date":"2024-04-29T12:12:52+08:00","permalink":"https://arlettebrook.github.io/p/go%E7%AC%AC%E4%B8%89%E6%96%B9log%E5%BA%93%E4%B9%8Bzap/","title":"Go第三方log库之zap"},{"content":" 概述 大多数语言都有“依赖”、“包”等概念，Go语言的依赖处理经历了几次变革。\n最早的时候，Go所依赖的所有的第三方库都放在GOPATH这个目录下面。从v1.5开始引入vendor模式，如果项目目录下有vendor目录，那么go工具链会优先使用vendor内的包进行编译、测试等。\n从v1.11开始，引入了Go Modules 作为依赖解决方案，到v1.14宣布Go Modules已经可以用于生产环境，到v1.16版本开始Go Module默认开启。\n什么是 Go Modules Go modules 是 Go 语言的依赖解决方案，发布于 Go1.11，成长于 Go1.12，丰富于 Go1.13，正式于 Go1.14 推荐在生产上使用。\nGo Modules使得Go语言开发者能够更方便地管理代码包及其版本，并能够与现有的版本控制工具（如Git、SVN等）集成使用。\n在传统的GOPATH模式中，所有Go代码都必须位于一个全局的GOPATH路径之下，这使得在不同项目中使用不同版本的依赖包变得非常困难。然而，在Go Modules模式下，每个项目都可以独立管理自己的依赖关系，具有更好的兼容性。当使用Go Modules模式后，项目中会自动创建go.mod文件，其中记录了项目所依赖的模块及其版本信息。go.mod是Go语言项目中的模块文件，用于管理项目的依赖关系和版本信息。\nGo Modules也支持语义化版本控制，这意味着开发者可以指定依赖包的版本范围，而不是仅仅依赖最新的版本。这种灵活性有助于确保项目的稳定性和可维护性。\nGo moudles 目前集成在 Go 的工具链中，只要安装了 Go，自然而然也就可以使用 Go moudles 了，而 Go modules 的出现也解决了在 Go1.11 前的几个常见争议问题：\nGo 语言长久以来的依赖管理问题。 “淘汰”现有的 GOPATH 的使用模式。 统一社区中的其它的依赖管理工具（提供迁移功能）。 优势\n首先，研发者能够在任何目录下工作，而不仅仅是在GOPATH指定的目录。 可以安装依赖包的指定版本，而不是只能从master分支安装最新的版本。 可以导入同一个依赖包的多个版本。当我们老项目使用老版本，新项目使用新版本时会非常有用。 要有一个能够罗列当前项目所依赖包的列表。这个的好处是当我们发布项目时不用同时发布所依赖的包。Go能够根据该文件自动下载对应的包。 GO PATH介绍 安装好go开发环境之后，可以运行go env查看go运行时的环境变量。要修改这些环境变量，可以通过配置环境变量来覆盖默认值(覆盖了就不能通过命令设置)，如临时设置export GO111MODULE=on。或者通过命令go env -w key=value，如go env -w GO111MODULE=on。通过命令修改的环境变量保存在GOENV这个环境变量指向的文件。\n有两个比较重要的环境变量：\nGOROOT：Golang 安装目录的路径，包含编译器程序和系统包，也可以放置三方包（不推荐）。新版本已经不需要配置这个环境变量了，安装了go会自动推断出该变量的值。如果安装之后环境变量中没有$GORROOT/bin,需要手动添加，这样才能直接在命令行中运行go编译程序。 GOPATH：该工作目录，放置编译后二进制和 import 包时的搜索路径，一般有三个目录: bin、pkg、src。并且该环境变量必须手动设置。 bin：用来存放编译后的可执行文件。引入Go modules之后用于存放get install安装的可执行文件。 pkg：存储预编译的目标文件，以加快程序的后续编译速度。引入Go modules之后用于存放第三方包。 src：存储所有.go文件或源代码。在编写 Go 应用程序，程序包和库时，一般会以$GOPATH/src/github.com/foo/bar的路径进行存放。引入Go modules之后用一般不用，go项目可以放在任意目录中，不在是$GOPATH/src 因此在使用 GOPATH 模式下，我们需要将应用代码存放在固定的$GOPATH/src目录下，并且如果执行go get来拉取外部依赖会自动下载并安装到$GOPATH目录下。\nGOPATH模式的弊端 在 GOPATH 的 $GOPATH/src 下进行 .go 文件或源代码的存储，我们可以称其为 GOPATH 的模式，这个模式拥有一些弊端。\nA. 无版本控制概念. 在执行go get的时候，你无法传达任何的版本信息的期望，也就是说你也无法知道自己当前更新的是哪一个版本，也无法通过指定来拉取自己所期望的具体版本。 B.无法同步一致第三方版本号. 在运行 Go 应用程序的时候，你无法保证其它人与你所期望依赖的第三方库是相同的版本，也就是说在项目依赖库的管理上，你无法保证所有人的依赖版本都一致。 C.无法指定当前项目引用的第三方版本号. 你没办法处理 v1、v2、v3 等等不同版本的引用问题，因为 GOPATH 模式下的导入路径都是一样的，都是github.com/foo/bar。 Go 语言官方从 Go1.11 起开始推进 Go modules（前身vgo，知道即可，不需要深入了解），Go1.13 起不再推荐使用 GOPATH 的使用模式，Go modules 也渐趋稳定，因此新项目也没有必要继续使用GOPATH模式。\nGo Module 语义化版本规范 Go Module 的设计采用了语义化版本规范，语义化版本规范非常流行且具有指导意义，本文就来聊聊语义化版本规范的设计和在 Go 中的应用。\n语义化版本规范 语义化版本规范（SemVer）是由 Gravatars 创办者兼 GitHub 共同创办者 Tom Preston-Werner 所建立，旨在解决 依赖地狱 问题。\n它清楚明了的规定了版本格式、版本号递增规：\n版本格式：采用 X.Y.Z 的格式，X 是主版本号、Y 是次版本号、而 Z 为修订号（即：主版本号.次版本号.修订号），其中 X、Y 和 Z 为非负的整数，且禁止在数字前方补零。\n版本号递增规则：\n主版本号：当做了不兼容的 API 修改。\n次版本号：当做了向下兼容的功能性新增及修改。\n修订号：当做了向下兼容的问题修正。\n另外，先行版本号 及 版本编译信息 可以加到 主版本号.次版本号.修订号 的后面，作为延伸。\n完整版本格式如下：\n先行版本号可以有多个，如第一个为UTC时间，第二个为提交的哈希值：\n1 2 v4.0.1-0.20210109023952-943e75fe5223+incompatible v0.0.0-20240416160154-fe59bbe5cc7f 其中版本号核心部分 X.Y.Z 是必须的，使用 . 连接，先行版本号和版本编译信息是可选的，先行版本号通过 - 与核心部分连接，版本编译信息通过 + 与核心部分或先行版本号连接。\n合法的几种版本号格式如下：\n主版本号.次版本号.修订号 主版本号.次版本号.修订号-先行版本号 主版本号.次版本号.修订号+版本编译信息 主版本号.次版本号.修订号-先行版本号+版本编译信息 主版本号必须在有任何不兼容的修改被加入公共 API 时递增。每当主版本号递增时，次版本号和修订号必须归零。\n次版本号必须在有向下兼容的新功能出现或有改进时递增，或在任何公共 API 的功能被标记为弃用时也必须递增。每当次版本号递增时，修订号必须归零。\n修订号必须在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。\n存在先行版本号，意味着当前版本不够稳定，且可能存在兼容性问题。先行版本号是一连串以 . 分隔的标识符，由 ASCII 字母数字和连接号 [0-9A-Za-z-] 组成，禁止出现空白符，数字类型则禁止在前方补零。合法示例：1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。\n版本编译信息标志符规格与先行版本号基本相同，略有差异的是数字类型前方允许补零。合法示例：1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。\n除了上面几点说明，还需要额外关注以下几点：\n标记版本号的软件发行后，禁止改变该版本软件的内容。任何修改都必须以新版本发行。 主版本号为零（0.y.z）的软件处于开发初始阶段，一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。 1.0.0 的版本号用于界定公共 API 的形成。这一版本之后所有的版本号更新都基于公共 API 及其修改内容。 社区中还存在一个不成文的规定，对于次版本号，偶数为稳定版本，奇数为开发版本。当然不是所有项目都这样设计。 使用语义化版本规范可能遇到的问题 在使用语义化版本规范过程中，可能人为或程序编写错误导致出现如下几种可预见的问题：\n万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办？\n一旦发现自己破坏了语义化版本控制的规范，就要修正这个问题，并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况，也不能去修改已发行的版本。可以的话，将有问题的版本号记录到文档中，告诉使用者问题所在，让他们能够意识到这是有问题的版本。\n注意：不到万不得已，不要也不能去修改已发行的版本。\n如果我变更了公共 API 但无意中未遵循版本号的改动怎么办呢？（意即在修订等级的发布中，误将重大且不兼容的改变加到代码之中）\n自行做最佳的判断。如果你有庞大的使用者群在依照公共 API 的意图而变更行为后会大受影响，那么最好做一次主版本的发布，即使严格来说这个修复仅是修订等级的发布。记住，语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的，那就透过版本号来向他们说明。\nv1.2.3 是一个语义化版本号吗？\nv1.2.3 并不是的一个语义化的版本号。但是，在语义化版本号之前增加前缀 v 是用来表示版本号的常用做法。在版本控制系统中，将 version 缩写为 v 是很常见的。比如：git tag v1.2.3 -m \u0026quot;Release version 1.2.3\u0026quot; 中，v1.2.3 表示标签名称，而 1.2.3 是语义化版本号。go modules的模块版本也是在前面加v\n如何验证语义化版本规范正确性 官方提供了两个正则可以检查语义化版本号的正确性。\n支持按组名称提取匹配结果\n1 ^(?P\u0026lt;major\u0026gt;0|[1-9]\\d*)\\.(?P\u0026lt;minor\u0026gt;0|[1-9]\\d*)\\.(?P\u0026lt;patch\u0026gt;0|[1-9]\\d*)(?:-(?P\u0026lt;prerelease\u0026gt;(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(?P\u0026lt;buildmetadata\u0026gt;[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$ Go 语言示例：\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 package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;regexp\u0026#34; ) func main() { version := \u0026#34;0.1.2-alpha+001\u0026#34; pattern := regexp.MustCompile(`^(?P\u0026lt;major\u0026gt;0|[1-9]\\d*)\\.(?P\u0026lt;minor\u0026gt;0|[1-9]\\d*)\\.(?P\u0026lt;patch\u0026gt;0|[1-9]\\d*)(?:-(?P\u0026lt;prerelease\u0026gt;(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(?P\u0026lt;buildmetadata\u0026gt;[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$`) r := pattern.FindStringSubmatch(version) m := make(map[string]string) for i, name := range pattern.SubexpNames() { if i == 0 { m[\u0026#34;version\u0026#34;] = r[i] } else { m[name] = r[i] } } result, _ := json.MarshalIndent(m, \u0026#34;\u0026#34;, \u0026#34; \u0026#34;) fmt.Printf(\u0026#34;%s\\n\u0026#34;, result) } /* { \u0026#34;buildmetadata\u0026#34;: \u0026#34;001\u0026#34;, \u0026#34;major\u0026#34;: \u0026#34;0\u0026#34;, \u0026#34;minor\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;patch\u0026#34;: \u0026#34;2\u0026#34;, \u0026#34;prerelease\u0026#34;: \u0026#34;alpha\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;0.1.2-alpha+001\u0026#34; } */ 支持按编号提取匹配结果\n1 ^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$ Go 语言示例：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;regexp\u0026#34; ) func main() { version := \u0026#34;0.1.2-alpha+001\u0026#34; pattern := regexp.MustCompile(`^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$`) r := pattern.FindStringSubmatch(version) for i, s := range r { fmt.Printf(\u0026#34;%d -\u0026gt; %s\\n\u0026#34;, i, s) } } /* 0 -\u0026gt; 0.1.2-alpha+001 1 -\u0026gt; 0 2 -\u0026gt; 1 3 -\u0026gt; 2 4 -\u0026gt; alpha 5 -\u0026gt; 001 */ Go Modules版本设计 依赖地狱 我们先来看下早期 Go 依赖包存在的依赖地狱问题：\n首先存在两个包 pkg1 和 pkg2，分别依赖 pkg3 的 v1.0.0 版本和 v2.0.0 版本，现在我们开发一个 app 包，它依赖 pkg1 和 pkg2，那么此时由于 app 包只允许包含一个 pkg3 依赖，所以 Go 构建工具无法抉择应该使用哪个版本的 pkg3。这就是所谓的依赖地狱问题。\n语义导入版本 为了解决依赖地狱问题，Go 在 1.11 版本时引入和 Go Modules：\nGo Module 解决问题的方式是，把 pkg3 的 v1.0.0 版本和 v2.0.0 版本当作两个不同的包，这样也就允许了 app 包能够同时包含多个不同版本的 pkg3。\n在使用时，需要在包的导入路径上加上包的主版本号。这里以 go-micro 包使用为例，展示下 Go Module 语义导入版本的用法：\n1 2 3 4 5 6 7 8 9 10 11 12 import \u0026#34;go-micro.dev/v4\u0026#34; // create a new service service := micro.NewService( micro.Name(\u0026#34;helloworld\u0026#34;), ) // initialise flags service.Init() // start the service service.Run() 可以看到导入路径为 \u0026quot;go-micro.dev/v4\u0026quot;，其中 v4 就代表了需要引入 go-micro 的 v4.y.z 版本。\nGo Modules基本使用 go modules相关命令 在 Go modules 中，我们能够使用如下命令进行操作：\n命令 介绍 go mod init \u0026lt;project\u0026gt; 初始化项目依赖，生成go.mod模块文件 go mod download 根据go.mod文件下载依赖 go mod tidy 比对项目文件中引入的依赖与go.mod进行比对,整理模块文件，去除没有用到的依赖 go mod graph 输出依赖关系图、查看现有的依赖结构 go mod edit 编辑go.mod文件 go mod vendor 将项目的所有依赖导出至vendor目录 go mod verify 检验一个依赖包是否被篡改过 go mod why 解释为什么需要某个依赖 go modules参数配置 GO111MODULE\nGo语言提供了 GO111MODULE 这个环境变量来作为 Go modules 的开关，其允许设置以下参数：\n参数 说明 auto 只要项目包含了 go.mod 文件的话启用 Go modules，目前在 Go1.11 至 Go1.14 中仍然是默认值。 on 启用 Go modules，推荐设置，将会是Go1.16版本之后的默认值。 off 禁用 Go modules，不推荐设置。 你可能会留意到 GO111MODULE 这个名字比较“奇特”，实际上在 Go 语言中经常会有这类阶段性的变量， GO111MODULE 这个命名代表着Go语言在 1.11 版本添加的。后续版本中可能会去掉。\nGOPROXY\n这个环境变量主要是用于设置 Go 模块代理（Go module proxy），其作用是用于使 Go 在后续拉取模块版本时能够脱离传统的 VCS（版本控制系统，如github，就是源地址下载） 方式，直接通过镜像站点来快速拉取。值为off表示禁止模块代理。\n设置GOPROXY可以加速模块下载，确保构建确定性（提供稳定的构建版本），提高安全性，确保模块始终可用。\nGOPROXY 的默认值是：https://proxy.golang.org,direct，由于某些原因国内无法正常访问该地址，所以我们通常需要配置一个可访问的地址。目前国内社区使用比较多的有两个 https://goproxy.cn和 https://goproxy.io，当然如果你的公司有提供GOPROXY地址那么就直接使用。并且修改的代理，通过go get命令下载自己的公共模块，也会同步到 https://pkg.go.dev/。\n设置GOPAROXY的命令如下：\n1 go env -w GOPROXY=https://goproxy.cn,direct GOPROXY 允许设置多个代理地址，多个地址之间需使用英文逗号 “,” 分隔。最后的 “direct” 是一个特殊指示符，用于指示 Go 回源到源地址去抓取（比如 GitHub 等）。当配置有多个代理地址时，如果第一个代理地址返回 404 或 410 错误时，Go 会自动尝试下一个代理地址，当遇见 “direct” 时触发回源，也就是回到源地址去抓取。就是代理失败之后用传统方式（源地址下载模块）。\nGOPRIVATE\nGONOPROXY/GONOSUMDB/GOPRIVATE\n这三个环境变量都是用在当前项目依赖了私有模块，例如像是你公司的私有 git 仓库，又或是 github 中的私有库，都是属于私有模块，都是要进行设置的，否则会拉取失败。\n更细致来讲，就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database 都无法访问到的模块时的场景。\n而一般建议直接设置 GOPRIVATE，它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值，所以建议的最佳姿势是直接使用 GOPRIVATE。\n设置了GOPROXY 之后，go 命令就会从配置的代理地址拉取和校验依赖包。当我们在项目中引入了非公开的包（公司内部git仓库或 github 私有仓库等），此时便无法正常从代理拉取到这些非公开的依赖包，这个时候就需要配置 GOPRIVATE 环境变量。GOPRIVATE用来告诉 go 命令哪些仓库属于私有仓库，不必通过代理服务器拉取和校验。\nGOPRIVATE 的值也可以设置多个，多个地址之间使用英文逗号 “,” 分隔。我们通常会把自己公司内部的代码仓库设置到 GOPRIVATE 中，例如：\n1 $ go env -w GOPRIVATE=\u0026#34;git.example.com,github.com/arlettebrook/demo\u0026#34; 设置后，前缀为 git.xxx.com 和 github.com/arlettebrook/demo的模块都会被认为是私有模块。\n如果不想每次都重新设置，我们也可以利用通配符，例如：\n1 $ go env -w GOPRIVATE=\u0026#34;*.example.com\u0026#34; 这样子设置的话，所有模块路径为 example.com 的子域名（例如：git.example.com）都将不经过 Go module proxy 和 Go checksum database，需要注意的是不包括 example.com 本身。\n此外，如果公司内部自建了 GOPROXY 服务，那么我们可以通过设置 GONOPROXY=none，允许通内部代理拉取私有仓库的包。\ngo modules模块文件 初识化项目\n在项目的根目录下运行go mod init \u0026lt;project\u0026gt;，如go mod init github.com/arlettebrook/demo，demo是项目名，github.com/arlettebrook/demo是模块导入路径，当导入的时候，如果本地没有，会去该路径下载。\ngo.mod 文件\n在初始化项目时，会生成一个 go.mod 文件，是启用了 Go modules 项目所必须的最重要的标识，同时也是 GO111MODULE 值为 auto 时的识别标识，它描述了当前项目（也就是当前模块）的元信息，每一行都以一个动词开头。\n示例文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 module github.com/arlettebrook/demo go 1.22.1 require ( example.com/apple v0.1.2 example.com/banana v1.2.3 example.com/banana/v2 v2.3.4 example.com/pear // indirect example.com/strawberry // incompatible ) exclude example.com/banana v1.2.4 replace example.com/apple v0.1.2 =\u0026gt; example.com/fried v0.1.0 replace example.com/banana =\u0026gt; example.com/fish 说明\nmodule：用于定义当前项目的模块路径。 go：用于标识当前模块的 Go 语言版本，值为初始化模块时的版本，目前来看还只是个标识作用。 require：用于设置一个特定的模块版本。 exclude：用于从使用中排除一个特定的模块版本。 replace：用于将一个模块版本替换为另外一个模块版本。 另外你会发现 example.com/pear 的后面会有一个 indirect 标识，indirect 标识表示该模块为间接依赖，也就是在当前应用程序中的 import 语句中，并没有发现这个模块的明确引用，有可能是你先手动 go get 拉取下来的，也有可能是你所依赖的模块所依赖的，情况有好几种。incompatible：不兼容的\ngo.sum 文件\n在第一次拉取模块依赖后，会发现多出了一个 go.sum 文件，其详细罗列了当前项目直接或间接依赖的所有模块版本，并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。\n1 2 3 4 github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 可以看到一个模块路径可能有如下两种：\n1 2 github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= h1 hash 是 Go modules 将目标模块版本的 zip 文件开包后，针对所有包内文件依次进行 hash，然后再把它们的 hash 结果按照固定格式和算法组成总的 hash 值。\n而 h1 hash 和 go.mod hash 两者，要不就是同时存在，要不就是只存在 go.mod hash。那什么情况下会不存在 h1 hash 呢，就是当 Go 认为肯定用不到某个模块版本的时候就会省略它的 h1 hash，就会出现不存在 h1 hash，只存在 go.mod hash 的情况。\ngo.mod和go.sum都应该被提交到git仓库中去。当别人使用你的项目时，mod保证依赖版本一直，sum保证依赖不被篡改。\ngo modules模块下载 我们下载、添加模块使用go get -u \u0026lt;module path\u0026gt;。\n默认下载、添加最新版本，首先会检查本地（pkg：全局模块缓存）是否存在，没有，在去下载。\n在项目中下载会自动添加到go.mod文件中。\n-u选项会更新模块的依赖包到最新版本，推荐加上。\n还可以指定下载版本\n命令 作用 go get golang.org/x/text@latest 拉取最新的版本，若存在tag，则优先使用。可以省略。 go get golang.org/x/text@master 拉取 master 分支的最新 commit。@branch go get golang.org/x/text@v0.3.2 拉取 tag 为 v0.3.2 的 commit。@version，version必须满足语义化版本规范且前面加v。 go get golang.org/x/text@342b2e 拉取 hash 为 342b231 的 commit，最终会被转换为 v0.3.2。@commit go get golang.org/x/text/v2 下载主版本号为2的最新版 最新版本的选择\n分两种情况\n最新版本有发布tags：就以发布的版本，version一般为标签名，如v2.1.2 最新版本没有发布tags:就以提交的最新版本，version一般为已发布标签-最新提交日期-最新提交哈希+版本编译信息，版本编译信息一般没有。如v2.1.2-20240416160154-fe59bbe5cc7f，如果一次tags也没有发布，版本号则为v0.0.0，如v0.0.0-20240416160154-fe59bbe5cc7f 子模块同理 go modules全局缓存 Go module 会把下载到本地的依赖包会以类似下面的形式保存在 $GOPATH/pkg/mod目录下，每个依赖包都会带有版本号进行区分，这样就允许在本地存在同一个包的多个不同版本。\n1 2 3 4 5 6 7 mod ├── cache ├── github.com ├── golang.org ├── google.golang.org ├── gopkg.in ... 如果想清除所有本地已缓存的依赖包数据，可以执行 go clean -modcache 命令。\ngo modules模块导入 go模块导入用import \u0026quot;模块路径\u0026quot;\n当导入多个模块的时候用\n1 2 3 4 5 import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;path/filepath\u0026#34; ) 别名导入用import 别名 \u0026quot;模块路径\u0026quot;\n1 import f \u0026#34;fmt\u0026#34; 点导入用import . \u0026quot;模块路径\u0026quot;\n点导入是一种特殊的导入方式，它将包中的所有公共标识符（函数、变量、类型等）提升到当前文件的命名空间中，这样在代码中就可以直接使用这些标识符，而不需要加上包名前缀。但是，这种方式可能会导致命名冲突和代码可读性下降，因此一般不建议使用。\n空导入用import _ \u0026quot;模块路径\u0026quot;\n空导入通常用于初始化包中的变量或者执行包中的初始化函数，而不直接使用该包中的其他标识符。\n注意事项\n当模块的主版本号为0或1的时候省略了主版本标识。\n当主版本号为2及以上时，不能省略主版本标识。否则会出现冲突。\n主版本标识只能为/v主版本号，不能用@version，一般使用主版本的最新版，这与语义化版本规范有关。\n如：\n1 2 import \u0026#34;github.com/jordan-wright/email\u0026#34; import \u0026#34;github.com/jordan-wright/email/v4\u0026#34; 为什么忽略 v0 和 v1 的主版本号\n还是与语义化版本规范有关，v0属于开发初始阶段，其公共api不被视为稳定版，当版本到达v1，其公共api基本确定，在此之后如果不出现不兼容api的修改，是不会修改主版本号的。后续的次版本、修订号会向下兼容。这是官方所鼓励的。当api做了不兼容的修改，主版本号就会修改。为了不出现冲突就会加上主版本标识。\ngopkg.in介绍 gopkg.in是旧go包管理工具中的一个，并不是官方包管理工具。作用是下载时重定向到相应github仓库。优点是： URL 更干净、更短、导入路径稳定、易于使用、支持版本控制。\n浏览器打开链接，会提供对应包的godoc在线链接以及github仓库链接。\ngopkg.in/ini.v1对应github仓库为githu.com/go-ini/ini，当没有指定用户名时，用户名默认为go-包名。\n1 2 gopkg.in/pkg.v3 → github.com/go-pkg/pkg (branch/tag v3, v3.N, or v3.N.M) gopkg.in/user/pkg.v3 → github.com/user/pkg (branch/tag v3, v3.N, or v3.N.M) 版本控制用.vNumber表示.。\n与go modules的区别：\nv1gopkg.in必须指定。go mod不用。 gopkg.in分隔符是.（go mod是/）。 v0为开发版、不稳定版，不指定默认为开发版，go mod不指定默认为v0或v1。 gopkg.in主版本为1就要指定主版本标识。go mod主版本为2才需要指定。 如何让gopkg.in收录自己的模块：\n与go mod一样，当我们使用go get下载已经存在的版本仓库时，会自动同步到在线的godoc中。 建议仓库名与用户名关系是pkg与go-pkg，推荐gopkg.in。当然也可以直接使用github仓库路径。 其他情况可以使用go mod。并通过像 proxy.golang.org 这样的代理服务器来分发你的模块。 gopkg.in版本控制同样遵循[语义化版本控制](#Go Module 语义化版本规范)。\n总结 至此我们大致介绍了 Go modules 的前世今生、语义化版本规范以及基本使用。\nGo modules 的成长和发展经历了一定的过程，如果你是刚接触的读者，直接基于 Go modules 的项目开始即可，如果既有老项目，那么是时候考虑切换过来了，Go1.14起已经准备就绪，并推荐你使用。\n参考 https://semver.org/lang/zh-CN/ Go Module 语义化版本规范 Go Modules详解 Go module详细介绍 ","date":"2024-04-28T10:57:17+08:00","permalink":"https://arlettebrook.github.io/p/go-modules%E8%AF%A6%E8%A7%A3/","title":"Go modules详解"},{"content":" Go 语言标准库中的 log 包设计简洁明了，易于上手，可以轻松记录程序运行时的信息、调试错误以及跟踪代码执行过程中的问题等。使用 log 包无需繁琐的配置即可直接使用。本文旨在深入探究 log 包的使用和原理，帮助读者更好地了解和掌握它。\n使用 先来看一个 log 包的使用示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package main import \u0026#34;log\u0026#34; func main() { log.Print(\u0026#34;Print\u0026#34;) log.Printf(\u0026#34;Printf: %s\u0026#34;, \u0026#34;print\u0026#34;) log.Println(\u0026#34;Println\u0026#34;) log.Fatal(\u0026#34;Fatal\u0026#34;) log.Fatalf(\u0026#34;Fatalf: %s\u0026#34;, \u0026#34;fatal\u0026#34;) log.Fatalln(\u0026#34;Fatalln\u0026#34;) log.Panic(\u0026#34;Panic\u0026#34;) log.Panicf(\u0026#34;Panicf: %s\u0026#34;, \u0026#34;panic\u0026#34;) log.Panicln(\u0026#34;Panicln\u0026#34;) } 假设以上代码存放在 main.go 中，通过 go run main.go 执行代码将会得到如下输出：\n1 2 3 4 5 6 $ go run main.go 2023/03/08 22:33:22 Print 2023/03/08 22:33:22 Printf: print 2023/03/08 22:33:22 Println 2023/03/08 22:33:22 Fatal exit status 1 以上示例代码中使用 log 包提供的 9 个函数分别对日志进行输出，最终得到 4 条打印日志。我们来分析下每个日志函数的作用，来看看为什么出现这样的结果。\nlog 包提供了 3 类共计 9 种方法来输出日志内容。\n函数名 作用 使用示例 Print 打印日志 log.Print(“Print”) Printf 打印格式化日志 log.Printf(“Printf: %s”, “print”) Println 打印日志并换行 log.Println(“Println”) Panic 打印日志后执行 panic(s)（s 为日志内容） log.Panic(“Panic”) Panicf 打印格式化日志后执行 panic(s) log.Panicf(“Panicf: %s”, “panic”) Panicln 打印日志并换行后执行 panic(s) log.Panicln(“Panicln”) Fatal 打印日志后执行 os.Exit(1) log.Fatal(“Fatal”) Fatalf 打印格式化日志后执行 os.Exit(1) log.Fatalf(“Fatalf: %s”, “fatal”) Fatalln 打印日志并换行后执行 os.Exit(1) log.Panicln(“Panicln”) 实际上log包每打印一句日志，都会换行，无论有没有ln或者\\n。\n根据以上表格说明，我们可以知道，log 包在执行 log.Fatal(\u0026quot;Fatal\u0026quot;) 时，程序打印完日志就通过 os.Exit(1) 退出了。这也就可以解释上面的示例程序，为什么打印了 9 次日志，却只输出了 4 条日志，并且最后程序退出码为 1 了。\n以上是 log 包最基本的使用方式，如果我们想对日志输出做一些定制，可以使用 log.New 创建一个自定义 logger：\n1 logger := log.New(os.Stdout, \u0026#34;[Debug] - \u0026#34;, log.Lshortfile) log.New 函数接收三个参数，分别用来指定：日志输出位置（一个 io.Writer 对象）、日志前缀（字符串，每次打印日志都会跟随输出）、日志属性（定义好的常量，稍后会详细讲解）。\n使用示例：\n1 2 3 4 5 6 7 8 9 10 11 package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) func main() { logger := log.New(os.Stdout, \u0026#34;[Debug] - \u0026#34;, log.Lshortfile) logger.Println(\u0026#34;custom logger\u0026#34;) } 示例输出：\n1 [Debug] - main.go:10: custom logger 以上示例中，指定日志输出到 os.Stdout，即标准输出；日志前缀 [Debug] - 会自动被加入到每行日志的行首；这条日志没有打印当前时间，而是打印了文件名和行号，这是 log.Lshortfile 日志属性的作用。\n日志属性可选项如下：\n属性 说明 Ldate 当前时区的日期，格式：2009/01/23 Ltime 当前时区的时间，格式：01:23:23 Lmicroseconds 当前时区的时间，格式：01:23:23.123123，精确到微妙 Llongfile 全文件名和行号，格式：/a/b/c/d.go:23 Lshortfile 当前文件名和行号，格式：d.go:23，会覆盖 Llongfile LUTC 使用 UTC 而非本地时区，推荐日志全部使用 UTC 时间 Lmsgprefix 将 prefix 内容从行首移动到日志内容前面 LstdFlags 标准 logger 对象的初始值（等于：`Ldate 这些属性都是预定义好的常量，不能修改，可以通过 | 运算符组合使用（如：log.Ldate|log.Ltime|log.Lshortfile）。\n使用 log.New 函数创建 logger 对象以后，依然可以通过 logger 对象的方法修改其属性值（默认的log也同样有下列同名函数）：\n方法 作用 SetOutput 设置日志输出位置 SetPrefix 设置日志输出前缀 SetFlags 设置日志属性 现在我们来看一个更加完整的使用示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) func main() { // 准备日志文件 logFile, _ := os.Create(\u0026#34;demo.log\u0026#34;) defer func() { _ = logFile.Close() }() // 初始化日志对象 logger := log.New(logFile, \u0026#34;[Debug] - \u0026#34;, log.Lshortfile|log.Lmsgprefix) logger.Print(\u0026#34;Print\u0026#34;) logger.Println(\u0026#34;Println\u0026#34;) // 修改日志配置 logger.SetOutput(os.Stdout) logger.SetPrefix(\u0026#34;[Info] - \u0026#34;) logger.SetFlags(log.Ldate|log.Ltime|log.LUTC) logger.Print(\u0026#34;Print\u0026#34;) logger.Println(\u0026#34;Println\u0026#34;) } 执行以上代码，得到 demo.log 日志内容如下：\n1 2 main.go:15: [Debug] - Print main.go:16: [Debug] - Println 控制台输出内容如下：\n1 2 [Info] - 2023/03/11 01:24:56 Print [Info] - 2023/03/11 01:24:56 Println 可以发现，在 demo.log 日志内容中，因为指定了 log.Lmsgprefix 属性，所以日志前缀 [Debug] - 被移动到了日志内容前面，而非行首。\n因为后续通过 logger.SetXXX 对 logger 对象的属性进行了动态修改，所以最后两条日志输出到系统的标准输出。\n以上，基本涵盖了 log 包的所有常用功能。接下来我们就通过走读源码的方式来更深入的了解 log 包了。\n源码 注意：本文以 Go 1.19.4 源码为例，其他版本可能存在差异。\nGo 标准库的 log 包代码量非常少，算上注释也才 400+ 行，非常适合初学者阅读学习。\n在上面介绍的第一个示例中，我们使用 log 包提供的 9 个公开函数对日志进行输出，并通过表格的形式分别介绍了函数的作用和使用示例，那么现在我们就来看看这几个函数是如何定义的：\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 45 46 47 48 49 50 51 52 53 func Print(v ...any) { if atomic.LoadInt32(\u0026amp;std.isDiscard) != 0 { return } std.Output(2, fmt.Sprint(v...)) } func Printf(format string, v ...any) { if atomic.LoadInt32(\u0026amp;std.isDiscard) != 0 { return } std.Output(2, fmt.Sprintf(format, v...)) } func Println(v ...any) { if atomic.LoadInt32(\u0026amp;std.isDiscard) != 0 { return } std.Output(2, fmt.Sprintln(v...)) } func Fatal(v ...any) { std.Output(2, fmt.Sprint(v...)) os.Exit(1) } func Fatalf(format string, v ...any) { std.Output(2, fmt.Sprintf(format, v...)) os.Exit(1) } func Fatalln(v ...any) { std.Output(2, fmt.Sprintln(v...)) os.Exit(1) } func Panic(v ...any) { s := fmt.Sprint(v...) std.Output(2, s) panic(s) } func Panicf(format string, v ...any) { s := fmt.Sprintf(format, v...) std.Output(2, s) panic(s) } func Panicln(v ...any) { s := fmt.Sprintln(v...) std.Output(2, s) panic(s) } 可以发现，这些函数代码主逻辑基本一致，都是通过 std.Output 输出日志。不同的是，PrintX 输出日志后程序就执行结束了；Fatal 输出日志后会执行 os.Exit(1)；而 Panic 输出日志后会执行 panic(s)。\n那么接下来就是要搞清楚这个 std 对象是什么，以及它的 Output 方法是如何定义的。\n我们先来看下 std 是什么：\n1 2 3 4 5 6 7 8 9 var std = New(os.Stderr, \u0026#34;\u0026#34;, LstdFlags) func New(out io.Writer, prefix string, flag int) *Logger { l := \u0026amp;Logger{out: out, prefix: prefix, flag: flag} if out == io.Discard { l.isDiscard = 1 } return l } 可以看到，std 其实就是使用 New 创建的一个 Logger 对象，日志输出到标准错误输出，日志前缀为空，日志属性为 LstdFlags。\n这跟我们上面讲的自定义日志对象 logger := log.New(os.Stdout, \u0026quot;[Debug] - \u0026quot;, log.Lshortfile) 方式如出一辙。也就是说，当我们通过 log.Print(\u0026quot;Print\u0026quot;) 打印日志时，其实使用的是 log 包内部已经定义好的 Logger 对象。\nLogger 定义如下：\n1 2 3 4 5 6 7 8 type Logger struct { mu sync.Mutex // 锁，保证并发情况下对其属性操作是原子性的 prefix string // 日志前缀，即 Lmsgprefix 参数值 flag int // 日志属性，用来控制日志输出格式 out io.Writer // 日志输出位置，实现了 io.Writer 接口即可，如 文件、os.Stderr buf []byte // 存储日志输出内容 isDiscard int32 // 当 out = io.Discard 是，此值为 1 } 其中，flag 和 isDiscard 这两个属性有必要进一步解释下。\n首先是 flag 用来记录日志属性，其合法值如下：\n1 2 3 4 5 6 7 8 9 10 const ( Ldate = 1 \u0026lt;\u0026lt; iota // the date in the local time zone: 2009/01/23 Ltime // the time in the local time zone: 01:23:23 Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime. Llongfile // full file name and line number: /a/b/c/d.go:23 Lshortfile // final file name element and line number: d.go:23. overrides Llongfile LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone Lmsgprefix // move the \u0026#34;prefix\u0026#34; from the beginning of the line to before the message LstdFlags = Ldate | Ltime // initial values for the standard logger ) 具体含义我就不再一一解释了，前文的表格已经写的很详细了。\n值得注意的是，这里在定义常量时，巧妙的使用了左移运算符 1 \u0026lt;\u0026lt; iota，使得常量的值呈现 1、2、4、8… 这样的递增效果。其实是为了位运算方便，通过对属性进行位运算，来决定输出内容，其本质上跟基于位运算的权限管理是一样的。所以在使用 log.New 新建 Logger 对象时可以支持 log.Ldate|log.Ltime|log.Lshortfile 这种形式设置多个属性。\nstd 对象的属性初始值 LstdFlags 也是在这里定义的。\n其次还有一个属性 isDiscard，是用来丢弃日志的。在上面介绍 PrintX 函数定义时，在输出日志前有一个 if atomic.LoadInt32(\u0026amp;std.isDiscard) != 0 的判断，如果结果为真，则直接 return 不记录日志。\n在 Go 标准库的 io 包里，有一个 io.Discard 对象，io.Discard 实现了 io.Writer，它执行 Write 操作后不会产生任何实际的效果，是一个用于丢弃数据的对象。比如有时候我们不在意数据内容，但可能存在数据不读出来就无法关闭连接的情况，这时候就可以使用 io.Copy(io.Discard, io.Reader) 将数据写入 io.Discard 实现丢弃数据的效果。\n使用 New 创建 Logger 对象时，如果 out == io.Discard 则 l.isDiscard 的值会被置为 1，所以使用 PrintX 函数记录的日志将会被丢弃，而 isDiscard 属性之所以是 int32 类型而不是 bool，是因为方便原子操作。\n现在，我们终于可以来看 std.Output 的实现了：\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 func (l *Logger) Output(calldepth int, s string) error { now := time.Now() // 获取当前时间 var file string var line int // 加锁，保证并发安全 l.mu.Lock() defer l.mu.Unlock() // 通过位运算来判断是否需要获取文件名和行号 if l.flag\u0026amp;(Lshortfile|Llongfile) != 0 { // 运行 runtime.Caller 获取文件名和行号比较耗时，所以先释放锁 l.mu.Unlock() var ok bool _, file, line, ok = runtime.Caller(calldepth) if !ok { file = \u0026#34;???\u0026#34; line = 0 } // 获取到文件行号后再次加锁，保证下面代码并发安全 l.mu.Lock() } // 清空上次缓存的内容 l.buf = l.buf[:0] // 格式化日志头信息（如：日期时间、文件名和行号、前缀）并写入 buf l.formatHeader(\u0026amp;l.buf, now, file, line) // 追加日志内容到 buf l.buf = append(l.buf, s...) // 保证输出日志以 \\n 结尾 if len(s) == 0 || s[len(s)-1] != \u0026#39;\\n\u0026#39; { l.buf = append(l.buf, \u0026#39;\\n\u0026#39;) } // 调用 Logger 对象的 out 属性的 Write 方法输出日志 _, err := l.out.Write(l.buf) return err } Output 方法代码并不多，基本逻辑也比较清晰，首先根据日志属性来决定是否需要获取文件名和行号，因为调用 runtime.Caller 是一个耗时操作，开销比较大，为了增加并发性，暂时将锁释放，获取到文件名和行号后再重新加锁。\n接下来就是准备日志输出内容了，首先清空 buf 中保留的上次日志信息，然后通过 formatHeader 方法格式化日志头信息，接着把日志内容也追加到 buf 中，在这之后有一个保证输出日志以 \\n 结尾的逻辑，来保证输出的日志都是单独一行的。不知道你有没有注意到，在前文的 log 包使用示例中，我们使用 Print 和 Println 两个方法时，最终日志输出效果并无差别，使用 Print 打印日志也会换行，其实就是这里的逻辑决定的。\n最后，通过调用 l.out.Write 方法，将 buf 内容输出。\nfunc Caller(skip int) (pc uintptr, file string, line int, ok bool)\n当skip为0时，获取调用该函数的函数信息，返回值包括程序计数器（pc：program counter栈帧的入口地址）、file调用函数所在文件的绝对路径、line调用行号，ok是否获取成功 当skip为1时，获取调用该函数的调用函数的信息，返回值包括程序计数器（pc：program counter栈帧的入口地址）、调用函数所在文件的绝对路径、调用行号，是否获取成功 以此类推 func FuncForPC(pc uintptr) *Func\n根据pc获取调用函数对象，name属性可以获取调用函数名. func CallersFrames(callers []uintptr) *Frames\n根据pc字节切片获取调用栈帧。next()方法获取栈帧。然后通过栈帧可以获取函数名，函数所在文件的绝对路径。调用行号。 1 2 3 4 5 6 7 8 9 frames := runtime.CallersFrames([]uintptr{pc}) // 遍历栈帧 for { frame, more := frames.Next() if !more { break } fmt.Printf(\u0026#34;函数名: %s, 文件: %s, 行号: %d\\n\u0026#34;, frame.Function, frame.File, frame.Line) } 我们来看下用来格式化日志头信息的 formatHeader 方法是如何定义：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) { // 如果没有设置 Lmsgprefix 属性，将日志前缀内容设置到行首 if l.flag\u0026amp;Lmsgprefix == 0 { *buf = append(*buf, l.prefix...) } // 判断是否设置了日期时间相关的属性 if l.flag\u0026amp;(Ldate|Ltime|Lmicroseconds) != 0 { // 是否设置 UTC 时间 if l.flag\u0026amp;LUTC != 0 { t = t.UTC() } // 是否设置日期 if l.flag\u0026amp;Ldate != 0 { year, month, day := t.Date() itoa(buf, year, 4) *buf = append(*buf, \u0026#39;/\u0026#39;) itoa(buf, int(month), 2) *buf = append(*buf, \u0026#39;/\u0026#39;) itoa(buf, day, 2) *buf = append(*buf, \u0026#39; \u0026#39;) } // 是否设置时间 if l.flag\u0026amp;(Ltime|Lmicroseconds) != 0 { hour, min, sec := t.Clock() itoa(buf, hour, 2) *buf = append(*buf, \u0026#39;:\u0026#39;) itoa(buf, min, 2) *buf = append(*buf, \u0026#39;:\u0026#39;) itoa(buf, sec, 2) if l.flag\u0026amp;Lmicroseconds != 0 { *buf = append(*buf, \u0026#39;.\u0026#39;) itoa(buf, t.Nanosecond()/1e3, 6) } *buf = append(*buf, \u0026#39; \u0026#39;) } } // 是否设置文件名和行号 if l.flag\u0026amp;(Lshortfile|Llongfile) != 0 { if l.flag\u0026amp;Lshortfile != 0 { short := file for i := len(file) - 1; i \u0026gt; 0; i-- { if file[i] == \u0026#39;/\u0026#39; { short = file[i+1:] break } } file = short } *buf = append(*buf, file...) *buf = append(*buf, \u0026#39;:\u0026#39;) itoa(buf, line, -1) *buf = append(*buf, \u0026#34;: \u0026#34;...) } // 如果设置了 Lmsgprefix 属性，将日志前缀内容放到日志头信息最后，也就是紧挨着日志内容前面 if l.flag\u0026amp;Lmsgprefix != 0 { *buf = append(*buf, l.prefix...) } } formatHeader 方法是 log 包里面代码量最多的一个方法，主要通过按位与（\u0026amp;）来计算是否设置了某个日志属性，然后根据日志属性来格式化头信息。\n其中多次调用 itoa 函数，itoa 顾名思义，将 int 转换成 ASCII 码，itoa 定义如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func itoa(buf *[]byte, i int, wid int) { // Assemble decimal in reverse order. var b [20]byte bp := len(b) - 1 for i \u0026gt;= 10 || wid \u0026gt; 1 { wid-- q := i / 10 b[bp] = byte(\u0026#39;0\u0026#39; + i - q*10) bp-- i = q } // i \u0026lt; 10 b[bp] = byte(\u0026#39;0\u0026#39; + i) *buf = append(*buf, b[bp:]...) } 这个函数短小精悍，它接收三个参数，buf 用来保存转换后的内容，i 就是带转换的值，比如 year、month 等，wid 表示转换后 ASCII 码字符串宽度，如果传进来的 i 宽度不够，则前面补零。比如传入 itoa(\u0026amp;b, 12, 3)，最终输出字符串为 012。\n'0'+i会隐式触发字符和ASCII之间的转换，0的ASCII码是48，加09就是对应09的ASCII码，超过57就是别的字符。\n至此，我们已经理清了 log.Print(\u0026quot;Print\u0026quot;) 是如何打印一条日志的，其函数调用流程如下：\n上面我们讲解了使用 log 包中默认的 std 这个 Logger 对象打印日志的调用流程。当我们使用自定义的 Logger 对象（logger := log.New(os.Stdout, \u0026quot;[Debug] - \u0026quot;, log.Lshortfile)）来打印日志时，调用的 loggger.Print 是一个方法，而不是 log.Print 这个包级别的函数，所以其实 Logger 结构体也实现了 9 种输出日志方法：\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 45 46 47 48 49 50 51 52 53 func (l *Logger) Print(v ...any) { if atomic.LoadInt32(\u0026amp;l.isDiscard) != 0 { return } l.Output(2, fmt.Sprint(v...)) } func (l *Logger) Printf(format string, v ...any) { if atomic.LoadInt32(\u0026amp;l.isDiscard) != 0 { return } l.Output(2, fmt.Sprintf(format, v...)) } func (l *Logger) Println(v ...any) { if atomic.LoadInt32(\u0026amp;l.isDiscard) != 0 { return } l.Output(2, fmt.Sprintln(v...)) } func (l *Logger) Fatal(v ...any) { l.Output(2, fmt.Sprint(v...)) os.Exit(1) } func (l *Logger) Fatalf(format string, v ...any) { l.Output(2, fmt.Sprintf(format, v...)) os.Exit(1) } func (l *Logger) Fatalln(v ...any) { l.Output(2, fmt.Sprintln(v...)) os.Exit(1) } func (l *Logger) Panic(v ...any) { s := fmt.Sprint(v...) l.Output(2, s) panic(s) } func (l *Logger) Panicf(format string, v ...any) { s := fmt.Sprintf(format, v...) l.Output(2, s) panic(s) } func (l *Logger) Panicln(v ...any) { s := fmt.Sprintln(v...) l.Output(2, s) panic(s) } 这 9 个方法跟 log 包级别的函数一一对应，用于自定义 Logger 对象。\n有一个值得注意的点，在这些方法内部，调用 l.Output(2, s) 时，第一个参数 calldepth 传递的是 2，这跟 runtime.Caller(calldepth) 函数内部实现有关，runtime.Caller 函数签名如下：\n1 func Caller(skip int) (pc uintptr, file string, line int, ok bool) runtime.Caller 返回当前 Goroutine 的栈上的函数调用信息（程序计数器、文件信息、行号），其参数 skip 表示当前向上层的栈帧数，0 代表当前函数，也就是调用 runtime.Caller 的函数，1 代表上一层调用者，以此类推。\n因为函数调用链为 main.go -\u0026gt; log.Print -\u0026gt; std.Output -\u0026gt; runtime.Caller，所以 skip 值即为 2：\n0: 表示 std.Output 中调用 runtime.Caller 所在的源码文件和行号。 1: 表示 log.Print 中调用 std.Output 所在的源码文件和行号。 2: 表示 main.go 中调用 log.Print 所在的源码文件和行号。 这样当代码出现问题时，就能根据日志中记录的函数调用栈来找到报错的源码位置了。\n另外，前文介绍过三个设置 Logger 对象属性的方法，分别是 SetOutput、SetPrefix、SetFlags，其实这三个方法各自还有与之对应的获取相应属性的方法，定义如下：\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 func (l *Logger) Flags() int { l.mu.Lock() defer l.mu.Unlock() return l.flag } func (l *Logger) SetFlags(flag int) { l.mu.Lock() defer l.mu.Unlock() l.flag = flag } func (l *Logger) Prefix() string { l.mu.Lock() defer l.mu.Unlock() return l.prefix } func (l *Logger) SetPrefix(prefix string) { l.mu.Lock() defer l.mu.Unlock() l.prefix = prefix } func (l *Logger) Writer() io.Writer { l.mu.Lock() defer l.mu.Unlock() return l.out } func (l *Logger) SetOutput(w io.Writer) { l.mu.Lock() defer l.mu.Unlock() l.out = w isDiscard := int32(0) if w == io.Discard { isDiscard = 1 } atomic.StoreInt32(\u0026amp;l.isDiscard, isDiscard) } 其实就是针对每个私有属性，定义了 getter、setter 方法，并且每个方法内部为了保证并发安全，都进行了加锁操作。\n当然，log 包级别的函数，也少不了这几个功能：\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 func Default() *Logger { return std } func SetOutput(w io.Writer) { std.SetOutput(w) } func Flags() int { return std.Flags() } func SetFlags(flag int) { std.SetFlags(flag) } func Prefix() string { return std.Prefix() } func SetPrefix(prefix string) { std.SetPrefix(prefix) } func Writer() io.Writer { return std.Writer() } func Output(calldepth int, s string) error { return std.Output(calldepth+1, s) // +1 for this frame. } 至此，log 包的全部代码我们就一起走读完成了。\n总结一下：log 包主要设计了 Logger 对象和其方法，并且为了开箱即用，在包级别又对外提供了默认的 Logger 对象 std 和一些包级别的对外函数。Logger 对象的方法，和包级别的函数基本上是一一对应的，签名一样，这样就大大降低了使用难度。并且log包是并发安全的。\n使用建议 关于 log 包的使用，我还有几条建议分享给你：\nlog 默认不支持 Debug、Info、Warn 等更细粒度级别的日志输出方法，不过我们可以通过创建多个 Logger 对象的方式来实现，只需要给每个 Logger 对象指定不同的日志前缀即可：\n1 2 3 4 loggerDebug = log.New(os.Stdout, \u0026#34;[Debug]\u0026#34;, log.LstdFlags) loggerInfo = log.New(os.Stdout, \u0026#34;[Info]\u0026#34;, log.LstdFlags) loggerWarn = log.New(os.Stdout, \u0026#34;[Warn]\u0026#34;, log.LstdFlags) loggerError = log.New(os.Stdout, \u0026#34;[Error]\u0026#34;, log.LstdFlags) log 包作为 Go 标准库，仅支持日志的基本功能，不支持记录结构化日志、日志切割、Hook 等高级功能，所以更适合小型项目使用，比如一个单文件的脚本。对于中大型项目，则推荐使用一些主流的第三方日志库，如 logrus、zap、glog 等。\n另外，如果你对 Go 标准日志库有所期待，Go 官方还打造了另一个日志库 slog 现已进入实验阶段，如果项目发展顺利，将可能成为 log 包的替代品。\n总结 本文我与读者一起深入探究了 Go log 标准库，首先向大家介绍了 log 包如何使用，接着又带领大家一起走读了 log 包的源码，最后我也给出了一些自己对 log 包的使用建议。\n参考：\nGo log 源码 Go 每日一库之 log 搬运自深入探究 Go log 标准库 ","date":"2024-04-27T16:54:38+08:00","permalink":"https://arlettebrook.github.io/p/%E6%B7%B1%E5%85%A5%E6%8E%A2%E7%A9%B6-go-log-%E6%A0%87%E5%87%86%E5%BA%93/","title":"深入探究 Go log 标准库"},{"content":" 在程序开发中，有些场景是我们经常会遇到的，软件行业的先行者们帮我们总结了一些解决常见场景编码问题的最佳实践，于是就有了设计模式。选项模式在 Go 语言开发中会经常用到，所以今天我们来介绍一下选项模式的应用。\n熟悉 Python 开发的同学都知道，Python 有默认参数的存在，使得我们在实例化一个对象的时候，可以根据需要来选择性的覆盖某些默认参数，以此来决定如何实例化对象。当一个对象有多个默认参数时，这个特性非常好用，能够优雅的简化代码。\n而 Go 语言从语法上是不支持默认参数的，所以为了实现既能通过默认参数创建对象，又能通过传递自定义参数创建对象，我们就需要通过一些编程技巧来实现。\n通过多构造函数实现 1 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 package main import \u0026#34;fmt\u0026#34; const ( defaultAddr = \u0026#34;127.0.0.1\u0026#34; defaultPort = 8000 ) type Server struct { Addr string Port int } func NewServer() *Server { return \u0026amp;Server{ Addr: defaultAddr, Port: defaultPort, } } func NewServerWithOptions(addr string, port int) *Server { return \u0026amp;Server{ Addr: addr, Port: port, } } func main() { s1 := NewServer() s2 := NewServerWithOptions(\u0026#34;localhost\u0026#34;, 8001) fmt.Println(s1) // \u0026amp;{127.0.0.1 8000} fmt.Println(s2) // \u0026amp;{localhost 8001} } 这里我们为 Server 结构体实现了两个构造函数，其中 NewServer 无需传递参数即可直接返回 Server 对象，NewServerWithOptions 则需要传递 addr 和 port 两个参数来构造 Server 对象。当我们无需对 Server 进行定制，通过默认参数创建的对象即可满足需求时，我们可以使用 NewServer 来生成对象（s1），而当我们需要对其进行定制时，则可以使用 NewServerWithOptions 来生成对象（s2）。\n通过默认参数选项实现 另外一种实现默认参数的方案是，我们可以为要生成的结构体对象定义一个选项结构体，用来生成要创建对象的默认参数，代码实现如下：\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 package main import \u0026#34;fmt\u0026#34; const ( defaultAddr = \u0026#34;127.0.0.1\u0026#34; defaultPort = 8000 ) type Server struct { Addr string Port int } type ServerOptions struct { Addr string Port int } func NewServerOptions() *ServerOptions { return \u0026amp;ServerOptions{ Addr: defaultAddr, Port: defaultPort, } } func NewServerWithOptions(opts *ServerOptions) *Server { return \u0026amp;Server{ Addr: opts.Addr, Port: opts.Port, } } func main() { s1 := NewServerWithOptions(NewServerOptions()) s2 := NewServerWithOptions(\u0026amp;ServerOptions{ Addr: \u0026#34;localhost\u0026#34;, Port: 8001, }) fmt.Println(s1) // \u0026amp;{127.0.0.1 8000} fmt.Println(s2) // \u0026amp;{localhost 8001} } 我们为 Server 结构体专门实现了一个 ServerOptions 用来生成默认参数，调用 NewServerOptions 函数即可获得默认参数配置，构造函数 NewServerWithOptions 接收一个 *ServerOptions 类型作为参数，所以我们可以直接将调用 NewServerOptions 函数的返回值传递给 NewServerWithOptions 来实现通过默认参数生成对象（s1），也可以通过手动构造 ServerOptions 配置来生成定制对象（s2）。\n通过选项模式实现 以上两种方式虽然都能够完成功能，但实现上却都不够优雅，接下来我们一起来看下如何通过选项模式更优雅的解决这个问题，代码实现如下：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package main import \u0026#34;fmt\u0026#34; const ( defaultAddr = \u0026#34;127.0.0.1\u0026#34; defaultPort = 8000 ) type Server struct { Addr string Port int } type ServerOptions struct { Addr string Port int } type ServerOption interface { apply(*ServerOptions) } type FuncServerOption struct { f func(*ServerOptions) } func (fo FuncServerOption) apply(option *ServerOptions) { fo.f(option) } func WithAddr(addr string) ServerOption { return FuncServerOption{ f: func(options *ServerOptions) { options.Addr = addr }, } } func WithPort(port int) ServerOption { return FuncServerOption{ f: func(options *ServerOptions) { options.Port = port }, } } func NewServer(opts ...ServerOption) *Server { options := ServerOptions{ Addr: defaultAddr, Port: defaultPort, } for _, opt := range opts { opt.apply(\u0026amp;options) } return \u0026amp;Server{ Addr: options.Addr, Port: options.Port, } } func main() { s1 := NewServer() s2 := NewServer(WithAddr(\u0026#34;localhost\u0026#34;), WithPort(8001)) s3 := NewServer(WithPort(8001)) fmt.Println(s1) // \u0026amp;{127.0.0.1 8000} fmt.Println(s2) // \u0026amp;{localhost 8001} fmt.Println(s3) // \u0026amp;{127.0.0.1 8001} } 乍一看，我们的代码复杂了很多，但其实都是定义上的复杂，调用构造函数生成对象的代码复杂度是没有改变的。\n在这里我们定义了 ServerOptions 结构体用来配置默认参数，因为这里 Addr 和 Port 都有默认参数，所以 ServerOptions 的定义和 Server 定义是一样的，但有一定复杂性的结构体中可能会有些参数没有默认参数，必须让用户来配置，这时 ServerOptions 的字段就会少一些，大家可以按需定义。\n同时，我们还定义了一个 ServerOption 接口和实现了此接口的 FuncServerOption 结构体，它们的作用是让我们能够通过 apply 方法为 ServerOptions 结构体单独配置某项参数。\n我们可以分别为每个默认参数都定义一个 WithXXX 函数用来配置参数，如这里定义的 WithAddr 和 WithPort ，这样用户就可以通过调用 WithXXX 函数来定制需要覆盖的默认参数。\n此时默认参数定义在构造函数 NewServer 中，构造函数的接收一个不定长参数，类型为 ServerOption，在构造函数内部通过一个 for 循环调用每个传进来的 ServerOption 对象的 apply 方法，将用户配置的参数依次赋值给构造函数内部的默认参数对象 options 中，以此来替换默认参数，for 循环执行完成后，得到的 options 对象将是最终配置，将其属性依次赋值给 Server 即可生成新的对象。\n总结 通过 s2 和 s3 的打印结果可以发现，使用选项模式实现的构造函数更加灵活，相较于前两种实现，选项模式中我们可以自由的更改其中任意一项或多项默认配置。\n虽然选项模式确实会多写一些代码，但多数情况下这是值得的，Google 的 gRPC 框架 Go 语言实现中创建 gRPC server 的构造函数 NewServer 就使用了选项模式，感兴趣的同学可以看下其源码的实现思想其实和这里的示例程序如出一辙。\n希望今天的分享能够给你带来一点帮助。\n原文地址：Go 常见设计模式之选项模式\n","date":"2024-04-25T23:22:54+08:00","permalink":"https://arlettebrook.github.io/p/go%E5%B8%B8%E8%A7%81%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E9%80%89%E9%A1%B9%E6%A8%A1%E5%BC%8F/","title":"Go常见设计模式：选项模式"},{"content":" 单例模式是设计模式中最简单的一种模式，单例模式能够确保无论对象被实例化多少次，全局都只有一个实例存在。根据单例模式的特性，我们可以将其应用到全局唯一性配置、数据库连接对象、文件访问对象等。Go 语言有多种方式可以实现单例模式，我们今天就来一起学习下。\n饿汉式 饿汉式实现单例模式非常简单，直接看代码：\n1 2 3 4 5 6 7 8 9 package singleton type Singleton struct{} var instance = \u0026amp;Singleton{} func GetSingleton() *Singleton { return instance } singleton 包在被导入时会自动初始化 instance 实例，使用时通过调用 singleton.GetSingleton() 函数即可获得 Singleton 这个结构体的单例对象。\n由于单例对象是在包加载时立即被创建出来，所以也就有了这个形象的名称叫作饿汉式。与之对应的另一种实现方式叫作懒汉式，当实例被第一次使用时才会被创建。\n需要注意的是，尽管饿汉式实现单例模式如此简单，但大多数情况下仍不被推荐使用，因为如果单例实例化时初始化内容过多，可能造成程序加载用时较长。\n懒汉式 接下来我们再来看下如何通过懒汉式实现单例模式：\n1 2 3 4 5 6 7 8 9 10 11 12 package singleton type Singleton struct{} var instance *Singleton func GetSingleton() *Singleton { if instance == nil { instance = \u0026amp;Singleton{} } return instance } 相较于饿汉式的实现，我们把实例化 Singleton 结构体部分的代码移到了 GetSingleton() 函数内部。这样一来，就将对象实例化的步骤延迟到了 GetSingleton() 被第一次调用时。\n通过 instance == nil 的判断来实现单例并不十分可靠，当有多个 goroutine 同时调用 GetSingleton() 时无法保证并发安全。\n支持并发的单例 如果你用 Go 语言写过并发编程，那么应该可以很快想到解决懒汉式单例模式并发安全问题的方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package singleton import \u0026#34;sync\u0026#34; type Singleton struct{} var instance *Singleton var mu sync.Mutex func GetSingleton() *Singleton { mu.Lock() defer mu.Unlock() if instance == nil { instance = \u0026amp;Singleton{} } return instance } 我们对代码的主要修改就是在 GetSingleton() 函数最开始加了如下两行代码：\n1 2 mu.Lock() defer mu.Unlock() 通过加锁的机制，就可以保证这个实现单例模式的函数是并发安全的。\n不过这样也有些问题，因为用了锁机制，每次调用 GetSingleton() 时程序都会进行加锁、解锁的步骤，这样会导致程序性能的下降。\n双重锁定 加锁导致程序性能下降，但我们又不得不用锁来保证程序的并发安全，于是有人想出了双重锁定（Double-Check Locking）的方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package singleton import \u0026#34;sync\u0026#34; type Singleton struct{} var instance *Singleton var mu sync.Mutex func GetSingleton() *Singleton { if instance == nil { mu.Lock() defer mu.Unlock() if instance == nil { instance = \u0026amp;Singleton{} } } return instance } 可以看到，所谓的双重锁定实际上就是在程序加锁前又加了一层 instance == nil 判断，这样就兼顾了性能和安全两个方面。\n不过这段代码看起来有些奇怪，既然外层已经判断了 instance == nil，加锁后却又进行了第二次 instance == nil 判断。其实外层的 instance == nil 判断是为了提高程序的执行效率，因为如果 instance 已经存在，则无需进入 if 逻辑，程序直接返回 instance 即可。这样就免去了原来每次调用 GetSingleton() 都上锁的操作，将加锁的粒度更加精细化。而内层的 instance == nil 判断则是考虑了并发安全，在极端情况下，多个 goroutine 同时走到了加锁这一步，内层判断就起到作用了。\nGopher 惯用方案 gopher原意地鼠，在golang 的世界里解释为地道的go程序员。在其他语言的世界里也有PHPer，Pythonic的说法，反而Java是个例外。虽然也有Javaer之类的说法，但似乎并不被认可。而地道或者说道地，说的是gopher写的代码无不透露出go的独特气息，比如项目结构、命名方式、代码格式、编码风格、构建方式等等。用gopher的话说，用go编写代码就像是在画一幅中国山水画，成品美不胜收，心旷神怡。\n虽然我们通过双重锁定机制兼顾和性能和并发安全，但代码有些丑陋，不符合广大 Gopher 的期待。好在 Go 语言在 sync 包中提供了 Once 机制能够帮助我们写出更加优雅的代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package singleton import \u0026#34;sync\u0026#34; type Singleton struct{} var instance *Singleton var once sync.Once func GetSingleton() *Singleton { once.Do(func() { instance = \u0026amp;Singleton{} }) return instance } Once 是一个结构体，在执行 Do 方法的内部通过 atomic 操作和加锁机制来保证并发安全，且 once.Do 能够保证多个 goroutine 同时执行时 \u0026amp;singleton{} 只被创建一次。\n其实 Once 并不神秘，其内部实现跟上面使用的双重锁定机制非常类似，只不过把 instance == nil 换成了 atomic 操作，感兴趣的同学可以查看下其对应源码。\n总结 以上就是 Go 语言中实现单例模式的几种常用套路，经过对比可以得出结论，最推荐的方式是使用 once.Do 来实现，sync.Once 包帮我们隐藏了部分细节，却可以让代码可读性得到很大提升。\n参考 不一样的go语言-gopher Go 常见设计模式之单例模式 ","date":"2024-04-25T18:00:25+08:00","permalink":"https://arlettebrook.github.io/p/go%E5%B8%B8%E8%A7%81%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/","title":"Go常见设计模式：单例模式"},{"content":" 引子 在工作中，我时不时地会需要在Go中调用外部命令。前段时间我做了一个工具，在钉钉群中添加了一个机器人，@这个机器人可以让它执行一些写好的脚本程序完成指定的任务。机器人倒是不难，照着钉钉开发者文档添加好机器人，然后@这个机器人就会向一个你指定的服务器发送一个POST请求，请求中会附带文本消息。所以我要做的就是搭一个Web服务器，可以用go原生的net/http包，也可以用gin/fasthttp/fiber这些Web框架。收到请求之后，检查附带文本中的关键字去调用对应的程序，然后返回结果。\ngo标准库中的os/exec包对调用外部程序提供了支持，本文详细介绍os/exec的使用姿势。\n运行命令 Linux中有个cal命令，它可以显示指定年、月的日历，如果不指定年、月，默认为当前时间对应的年月。如果使用的是Windows，推荐安装msys2，这个软件包含了绝大多数的Linux常用命令。\n那么，在Go代码中怎么调用这个命令呢？其实也很简单：\n1 2 3 4 5 6 7 func main() { cmd := exec.Command(\u0026#34;cal\u0026#34;) err := cmd.Run() if err != nil { log.Fatalf(\u0026#34;cmd.Run() failed: %v\\n\u0026#34;, err) } } 首先，我们调用exec.Command传入命令名，创建一个命令对象exec.Cmd。接着调用该命令对象的Run()方法运行它。\n如果你实际运行了，你会发现什么也没有发生，哈哈。事实上，使用os/exec执行命令，标准输出和标准错误默认会被丢弃。\n显示输出 exec.Cmd对象有两个字段Stdout和Stderr，类型皆为io.Writer。我们可以将任意实现了io.Writer接口的类型实例赋给这两个字段，继而实现标准输出和标准错误的重定向。io.Writer接口在 Go 标准库和第三方库中随处可见，例如*os.File、*bytes.Buffer、net.Conn。所以我们可以将命令的输出重定向到文件、内存缓存甚至发送到网络中。\n显示到标准输出 将exec.Cmd对象的Stdout和Stderr这两个字段都设置为os.Stdout，那么输出内容都将显示到标准输出：\n1 2 3 4 5 6 7 8 9 func main() { cmd := exec.Command(\u0026#34;cal\u0026#34;) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { log.Fatalf(\u0026#34;cmd.Run() failed: %v\\n\u0026#34;, err) } } 运行程序。我在git bash运行，得到如下结果：\n输出了中文，检查一下环境变量LANG的值，果然是zh_CN.UTF-8。如果想输出英文，可以将环境变量LANG设置为en_US.UTF-8：\n1 2 3 $ echo $LANG zh_CN.UTF-8 $ LANG=en_US.UTF-8 go run main.go 得到输出：\n输出到文件 打开或创建文件，然后将文件句柄赋给exec.Cmd对象的Stdout和Stderr这两个字段即可实现输出到文件的功能。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main() { f, err := os.OpenFile(\u0026#34;out.txt\u0026#34;, os.O_WRONLY|os.O_CREATE, os.ModePerm) if err != nil { log.Fatalf(\u0026#34;os.OpenFile() failed: %v\\n\u0026#34;, err) } cmd := exec.Command(\u0026#34;cal\u0026#34;) cmd.Stdout = f cmd.Stderr = f err = cmd.Run() if err != nil { log.Fatalf(\u0026#34;cmd.Run() failed: %v\\n\u0026#34;, err) } } os.OpenFile打开一个文件，指定os.O_CREATE标志让操作系统在文件不存在时自动创建一个，返回该文件对象*os.File。*os.File实现了io.Writer接口。\n运行程序：\n1 2 3 4 5 6 7 8 9 $ go run main.go $ cat out.txt November 2022 Su Mo Tu We Th Fr Sa 1 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 发送到网络 现在我们来编写一个日历服务，接收年、月信息，返回该月的日历。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func cal(w http.ResponseWriter, r *http.Request) { year := r.URL.Query().Get(\u0026#34;year\u0026#34;) month := r.URL.Query().Get(\u0026#34;month\u0026#34;) cmd := exec.Command(\u0026#34;cal\u0026#34;, month, year) cmd.Stdout = w cmd.Stderr = w err := cmd.Run() if err != nil { log.Fatalf(\u0026#34;cmd.Run() failed: %v\\n\u0026#34;, err) } } func main() { http.HandleFunc(\u0026#34;/cal\u0026#34;, cal) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 这里为了简单，错误处理都省略了。正常情况下，year和month参数都需要做合法性校验。exec.Command函数接收一个字符串类型的可变参数作为命令的参数：\n1 func Command(name string, arg ...string) *Cmd 保存到内存对象中 *bytes.Buffer同样也实现了io.Writer接口，故如果我们创建一个*bytes.Buffer对象，并将其赋给exec.Cmd的Stdout和Stderr这两个字段，那么命令执行之后，该*bytes.Buffer对象中保存的就是命令的输出。\n1 2 3 4 5 6 7 8 9 10 11 12 func main() { buf := bytes.NewBuffer(nil) cmd := exec.Command(\u0026#34;cal\u0026#34;) cmd.Stdout = buf cmd.Stderr = buf err := cmd.Run() if err != nil { log.Fatalf(\u0026#34;cmd.Run() failed: %v\\n\u0026#34;, err) } fmt.Println(buf.String()) } 运行：\n1 2 3 4 5 6 7 8 $ go run main.go November 2022 Su Mo Tu We Th Fr Sa 1 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 运行命令，然后得到输出的字符串或字节切片这种模式是如此的普遍，并且使用便利，os/exec包提供了一个便捷方法：CombinedOutput。\n输出到多个目的地 有时，我们希望能输出到文件和网络，同时保存到内存对象。使用go提供的io.MultiWriter可以很容易实现这个需求。io.MultiWriter很方便地将多个io.Writer转为一个io.Writer。\n我们稍微修改上面的web程序：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func cal(w http.ResponseWriter, r *http.Request) { year := r.URL.Query().Get(\u0026#34;year\u0026#34;) month := r.URL.Query().Get(\u0026#34;month\u0026#34;) f, _ := os.OpenFile(\u0026#34;out.txt\u0026#34;, os.O_CREATE|os.O_WRONLY, os.ModePerm) buf := bytes.NewBuffer(nil) mw := io.MultiWriter(w, f, buf) cmd := exec.Command(\u0026#34;cal\u0026#34;, month, year) cmd.Stdout = mw cmd.Stderr = mw err := cmd.Run() if err != nil { log.Fatalf(\u0026#34;cmd.Run() failed: %v\\n\u0026#34;, err) } fmt.Println(buf.String()) } 调用io.MultiWriter将多个io.Writer整合成一个io.Writer，然后将cmd对象的Stdout和Stderr都赋值为这个io.Writer。这样，命令运行时产出的输出会分别送往http.ResponseWriter、*os.File以及*bytes.Buffer。\n运行命令，获取输出 前面提到，我们常常需要运行命令，返回输出。exec.Cmd对象提供了一个便捷方法：CombinedOutput()。该方法运行命令，将输出内容以一个字节切片返回便于后续处理。所以，上面获取输出的程序可以简化为：\n1 2 3 4 5 6 7 8 9 func main() { cmd := exec.Command(\u0026#34;cal\u0026#34;) output, err := cmd.CombinedOutput() if err != nil { log.Fatalf(\u0026#34;cmd.Run() failed: %v\\n\u0026#34;, err) } fmt.Println(string(output)) } So easy!\nCombinedOutput()方法的实现很简单，先将标准输出和标准错误重定向到*bytes.Buffer对象，然后运行程序，最后返回该对象中的字节切片：\n1 2 3 4 5 6 7 8 9 10 11 12 13 func (c *Cmd) CombinedOutput() ([]byte, error) { if c.Stdout != nil { return nil, errors.New(\u0026#34;exec: Stdout already set\u0026#34;) } if c.Stderr != nil { return nil, errors.New(\u0026#34;exec: Stderr already set\u0026#34;) } var b bytes.Buffer c.Stdout = \u0026amp;b c.Stderr = \u0026amp;b err := c.Run() return b.Bytes(), err } CombinedOutput方法前几行判断表明，Stdout和Stderr必须是未设置状态。这其实很好理解，一般情况下，如果已经打算使用CombinedOutput方法获取输出内容，不会再自找麻烦地再去设置Stdout和Stderr字段了。\n与CombinedOutput类似的还有Output方法，区别是Output只会返回运行命令产出的标准输出内容。\n分别获取标准输出和标准错误 创建两个*bytes.Buffer对象，分别赋给exec.Cmd对象的Stdout和Stderr这两个字段，然后运行命令即可分别获取标准输出和标准错误。\n1 2 3 4 5 6 7 8 9 10 11 12 func main() { cmd := exec.Command(\u0026#34;cal\u0026#34;, \u0026#34;15\u0026#34;, \u0026#34;2012\u0026#34;) var stdout, stderr bytes.Buffer cmd.Stdout = \u0026amp;stdout cmd.Stderr = \u0026amp;stderr err := cmd.Run() if err != nil { log.Fatalf(\u0026#34;cmd.Run() failed: %v\\n\u0026#34;, err) } fmt.Printf(\u0026#34;output:\\n%s\\nerror:\\n%s\\n\u0026#34;, stdout.String(), stderr.String()) } 标准输入 exec.Cmd对象有一个类型为io.Reader的字段Stdin。命令运行时会从这个io.Reader读取输入。先来看一个最简单的例子：\n1 2 3 4 5 6 7 8 9 func main() { cmd := exec.Command(\u0026#34;cat\u0026#34;) cmd.Stdin = bytes.NewBufferString(\u0026#34;hello\\nworld\u0026#34;) cmd.Stdout = os.Stdout err := cmd.Run() if err != nil { log.Fatalf(\u0026#34;cmd.Run() failed: %v\\n\u0026#34;, err) } } 如果不带参数运行cat命令，则进入交互模式，cat按行读取输入，并且原样发送到输出。\n再来看一个复杂点的例子。Go标准库中compress/bzip2包只提供解压方法，并没有压缩方法。我们可以利用Linux命令bzip2实现压缩。bzip2从标准输入中读取数据，将其压缩，并发送到标准输出。\n1 2 3 4 5 6 7 8 9 10 11 12 func bzipCompress(d []byte) ([]byte, error) { var out bytes.Buffer cmd := exec.Command(\u0026#34;bzip2\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;-9\u0026#34;) cmd.Stdin = bytes.NewBuffer(d) cmd.Stdout = \u0026amp;out err := cmd.Run() if err != nil { log.Fatalf(\u0026#34;cmd.Run() failed: %v\\n\u0026#34;, err) } return out.Bytes(), nil } 参数-c表示压缩，-9表示压缩等级，9为最高。为了验证函数的正确性，写个简单的程序，先压缩\u0026quot;hello world\u0026quot;字符串，然后解压，看看是否能得到原来的字符串：\n1 2 3 4 5 6 7 func main() { data := []byte(\u0026#34;hello world\u0026#34;) compressed, _ := bzipCompress(data) r := bzip2.NewReader(bytes.NewBuffer(compressed)) decompressed, _ := ioutil.ReadAll(r) fmt.Println(string(decompressed)) } 运行程序，输出\u0026quot;hello world\u0026quot;。\n环境变量 环境变量可以在一定程度上微调程序的行为，当然这需要程序的支持。例如，设置ENV=production会抑制调试日志的输出。每个环境变量都是一个键值对。exec.Cmd对象中有一个类型为[]string的字段Env。我们可以通过修改它来达到控制命令运行时的环境变量的目的。\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;os/exec\u0026#34; ) func main() { cmd := exec.Command(\u0026#34;bash\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;./test.sh\u0026#34;) nameEnv := \u0026#34;NAME=darjun\u0026#34; ageEnv := \u0026#34;AGE=18\u0026#34; newEnv := append(os.Environ(), nameEnv, ageEnv) cmd.Env = newEnv out, err := cmd.CombinedOutput() if err != nil { log.Fatalf(\u0026#34;cmd.Run() failed: %v\\n\u0026#34;, err) } fmt.Println(string(out)) } 上面代码获取系统的环境变量，然后又添加了两个环境变量NAME和AGE。最后使用bash运行脚本test.sh：\n1 2 3 4 5 #!/bin/bash echo $NAME echo $AGE echo $GOPATH 程序运行结果：\n1 2 3 4 $ go run main.go darjun 18 D:\\workspace\\code\\go 检查命令是否存在 一般在运行命令之前，我们通过希望能检查要运行的命令是否存在，如果存在则直接运行，否则提示用户安装此命令。os/exec包提供了函数LookPath可以获取命令所在目录，如果命令不存在，则返回一个error。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main() { path, err := exec.LookPath(\u0026#34;ls\u0026#34;) if err != nil { fmt.Printf(\u0026#34;no cmd ls: %v\\n\u0026#34;, err) } else { fmt.Printf(\u0026#34;find ls in path:%s\\n\u0026#34;, path) } path, err = exec.LookPath(\u0026#34;not-exist\u0026#34;) if err != nil { fmt.Printf(\u0026#34;no cmd not-exist: %v\\n\u0026#34;, err) } else { fmt.Printf(\u0026#34;find not-exist in path:%s\\n\u0026#34;, path) } } 运行：\n1 2 3 $ go run main.go find ls in path:C:\\Program Files\\Git\\usr\\bin\\ls.exe no cmd not-exist: exec: \u0026#34;not-exist\u0026#34;: executable file not found in %PATH% 封装 执行外部命令的流程比较固定：\n调用exec.Command()创建命令对象； 调用Cmd.Run()执行命令 可以自己封装成一个工具包。\n总结 本文介绍了使用os/exec这个标准库调用外部命令的各种姿势。\n参考 Advanced command execution in go with os/exec: https://blog.kowalczyk.info/article/wOYk/advanced-command-execution-in-go-with-osexec.html Go中调用外部命令的几种姿势搬运自该篇文章。 ","date":"2024-04-24T23:41:31+08:00","permalink":"https://arlettebrook.github.io/p/go%E8%B0%83%E7%94%A8%E5%A4%96%E9%83%A8%E5%91%BD%E4%BB%A4os/exec%E5%BA%93%E4%BB%8B%E7%BB%8D/","title":"Go调用外部命令os/exec库介绍"},{"content":" 简介 今天我们来看一个很小，很实用的库go-homedir。顾名思义，go-homedir用来获取用户的主目录。 实际上，使用标准库os/user或者os.UserHomeDir()我们也可以得到这个信息：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os/user\u0026#34; ) func main() { u, err := user.Current() if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Home dir:\u0026#34;, u.HomeDir) } 那么为什么还要go-homedir库？\n在 Darwin 系统上，标准库os/user的使用需要 cgo。所以，任何使用os/user的代码都不能交叉编译。 但是，大多数人使用os/user的目的仅仅只是想获取主目录。因此，go-homedir库出现了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func UserHomeDir() (string, error) { env, enverr := \u0026#34;HOME\u0026#34;, \u0026#34;$HOME\u0026#34; switch runtime.GOOS { case \u0026#34;windows\u0026#34;: env, enverr = \u0026#34;USERPROFILE\u0026#34;, \u0026#34;%userprofile%\u0026#34; case \u0026#34;plan9\u0026#34;: env, enverr = \u0026#34;home\u0026#34;, \u0026#34;$home\u0026#34; } if v := Getenv(env); v != \u0026#34;\u0026#34; { return v, nil } // On some geese the home directory is not always defined. switch runtime.GOOS { case \u0026#34;android\u0026#34;: return \u0026#34;/sdcard\u0026#34;, nil case \u0026#34;ios\u0026#34;: return \u0026#34;/\u0026#34;, nil } return \u0026#34;\u0026#34;, errors.New(enverr + \u0026#34; is not defined\u0026#34;) } 还有就是官方的库也只是从环境变量中获取用户的家目录，没有考虑$HOME不存在的情况。\n快速使用 go-homedir是第三方包，使用前需要先安装：\n1 $ go get -u github.com/mitchellh/go-homedir 使用非常简单：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/mitchellh/go-homedir\u0026#34; ) func main() { dir, err := homedir.Dir() if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Home dir:\u0026#34;, dir) dir = \u0026#34;~/golang/src\u0026#34; expandedDir, err := homedir.Expand(dir) if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;Expand of %s is: %s\\n\u0026#34;, dir, expandedDir) } go-homedir有两个功能：\nDir：获取用户主目录； Expand：将路径中的第一个~扩展成用户主目录。 高级用法 由于Dir的调用可能涉及一些系统调用和外部执行命令，多次调用费性能。所以go-homedir提供了缓存的功能。默认情况下，缓存是开启的。 我们也可以将DisableCache设置为true来关闭它。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/mitchellh/go-homedir\u0026#34; ) func main() { homedir.DisableCache = true # 关闭了缓存 dir, err := homedir.Dir() if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Home dir:\u0026#34;, dir) } 使用缓存时，如果程序运行中修改了主目录，再次调用Dir还是返回之前的目录。如果需要获取最新的主目录，可以先调用Reset清除缓存。\n实现 go-homedir源码只有一个文件homedir.go，今天我们大概看一下Dir的实现，去掉缓存相关代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func Dir() (string, error) { var result string var err error if runtime.GOOS == \u0026#34;windows\u0026#34; { result, err = dirWindows() } else { // Unix-like system, so just assume Unix result, err = dirUnix() } if err != nil { return \u0026#34;\u0026#34;, err } return result, nil } 判断当前的系统是windows还是类 Unix，分别调用不同的方法。先看 windows 的，比较简单：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func dirWindows() (string, error) { // First prefer the HOME environmental variable if home := os.Getenv(\u0026#34;HOME\u0026#34;); home != \u0026#34;\u0026#34; { return home, nil } // Prefer standard environment variable USERPROFILE if home := os.Getenv(\u0026#34;USERPROFILE\u0026#34;); home != \u0026#34;\u0026#34; { return home, nil } drive := os.Getenv(\u0026#34;HOMEDRIVE\u0026#34;) path := os.Getenv(\u0026#34;HOMEPATH\u0026#34;) home := drive + path if drive == \u0026#34;\u0026#34; || path == \u0026#34;\u0026#34; { return \u0026#34;\u0026#34;, errors.New(\u0026#34;HOMEDRIVE, HOMEPATH, or USERPROFILE are blank\u0026#34;) } return home, nil } 流程如下：\n读取环境变量HOME，如果不为空，返回这个值； 读取环境变量USERPROFILE，如果不为空，返回这个值； 读取环境变量HOMEDRIVE和HOMEPATH，如果两者都不为空，拼接这两个值返回。 类 Unix 系统的实现稍微复杂一点：\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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 func dirUnix() (string, error) { homeEnv := \u0026#34;HOME\u0026#34; if runtime.GOOS == \u0026#34;plan9\u0026#34; { // On plan9, env vars are lowercase. homeEnv = \u0026#34;home\u0026#34; } // First prefer the HOME environmental variable if home := os.Getenv(homeEnv); home != \u0026#34;\u0026#34; { return home, nil } var stdout bytes.Buffer // If that fails, try OS specific commands if runtime.GOOS == \u0026#34;darwin\u0026#34; { cmd := exec.Command(\u0026#34;sh\u0026#34;, \u0026#34;-c\u0026#34;, `dscl -q . -read /Users/\u0026#34;$(whoami)\u0026#34; NFSHomeDirectory | sed \u0026#39;s/^[^ ]*: //\u0026#39;`) cmd.Stdout = \u0026amp;stdout if err := cmd.Run(); err == nil { result := strings.TrimSpace(stdout.String()) if result != \u0026#34;\u0026#34; { return result, nil } } } else { cmd := exec.Command(\u0026#34;getent\u0026#34;, \u0026#34;passwd\u0026#34;, strconv.Itoa(os.Getuid())) cmd.Stdout = \u0026amp;stdout if err := cmd.Run(); err != nil { // If the error is ErrNotFound, we ignore it. Otherwise, return it. if err != exec.ErrNotFound { return \u0026#34;\u0026#34;, err } } else { if passwd := strings.TrimSpace(stdout.String()); passwd != \u0026#34;\u0026#34; { // username:password:uid:gid:gecos:home:shell passwdParts := strings.SplitN(passwd, \u0026#34;:\u0026#34;, 7) if len(passwdParts) \u0026gt; 5 { return passwdParts[5], nil } } } } // If all else fails, try the shell stdout.Reset() cmd := exec.Command(\u0026#34;sh\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;cd \u0026amp;\u0026amp; pwd\u0026#34;) cmd.Stdout = \u0026amp;stdout if err := cmd.Run(); err != nil { return \u0026#34;\u0026#34;, err } result := strings.TrimSpace(stdout.String()) if result == \u0026#34;\u0026#34; { return \u0026#34;\u0026#34;, errors.New(\u0026#34;blank output when reading home directory\u0026#34;) } return result, nil } 流程如下：\n先读取环境变量HOME（注意 plan9 系统上为home），如果不为空，返回这个值； 使用getnet命令查看系统的数据库中的相关记录，我们知道passwd文件中存储了用户信息，包括用户的主目录。使用getent命令查看passwd中当前用户的那条记录，然后从中找到主目录部分返回； 如果上一个步骤失败了，我们知道cd后不加参数是直接切换到用户主目录的，而pwd可以显示当前目录。那么就可以结合这两个命令返回主目录。 这里分析源码并不是表示使用任何库都要熟悉它的源码，毕竟使用库就是为了方便开发。 但是源码是我们学习和提高的一个非常重要的途径。我们在使用库遇到问题的时候也要有能力从文档或甚至源码中查找原因。\n参考 home-dir GitHub 仓库 Go 每日一库之 go-homedir ","date":"2024-04-24T22:47:45+08:00","permalink":"https://arlettebrook.github.io/p/go-homedir%E5%BA%93%E4%BB%8B%E7%BB%8D/","title":"Go-homedir库介绍"},{"content":" 注意：要学会使用-h \u0026ndash;help选项，查看命令，看不懂在查阅。git help \u0026lt;command\u0026gt;可进入官方文档。Git入门参考。以下常用命令个人收集总结。\ngit简单命令 git init git init \u0026lt;directory\u0026gt;在指定的⽬录下创建⼀个空的git repo。不带参数将在当前⽬录下创建⼀个git repo。 git clone git clone \u0026lt;repo\u0026gt;克隆⼀个指定repo到本地。指定的repo可以是本地⽂件系统或者由HTTP或SSH指定的远程路径。 git clone -b \u0026lt;branch\u0026gt; \u0026lt;repo\u0026gt;克隆指定仓库的分支 git clone --recursive \u0026lt;repo\u0026gt;递归地克隆，克隆带有子模块的仓库 git clone --recurse-submodules \u0026lt;repository_url\u0026gt;同理 可组合使用 git add git add \u0026lt;directory\u0026gt;将指定⽬录的所有修改加⼊到下⼀次 commit中。把\u0026lt;directory\u0026gt;替换成\u0026lt;file\u0026gt;将添加指定⽂件的修改。 git add *、git add .、git add -A三条命令但是一样的，将所以修改提交到暂存区。 git commit 这个命令通常带-m选项git commit -m \u0026quot;\u0026lt;message\u0026gt;\u0026quot;提交暂存区的修改，使⽤指定的 \u0026lt;message\u0026gt;作为提交信息，⽽不是打开⽂本编辑器输⼊提交信息。 git commit -m \u0026lt;message\u0026gt; --amend将当前staged修改合并到最近⼀次的commit中。 git status git status显示哪些⽂件已被staged、以及未跟踪(untracked)。 git reflog git reflog显示本地repo的所有commit⽇志。 与git log的区别 log项目的提交历史，reflog本地仓库的引用提交日志。 引用会保留所以的提交历史，如何重置的历史。主要目的是提供一个安全网，以便在误操作（如错误的 git reset）后可以恢复丢失的提交或分支。 git log 的输出是永久性的，而 reflog 会在一段时间后自动过期（默认是 30 天），以节省空间。 git rm git rm fileName删除指定的文件。 与rm fileName的区别。 git rm不能删除未跟踪的文件， git rm删除之后直接到暂存区，而rm是到工作区 注意删除之后都需要提交操作。 撤销操作不用记，git都会有提示 git switch 该命令适用于特定git版本。\ngit switch \u0026lt;branch\u0026gt;切换到指定分支，\ngit checkout \u0026lt;branch\u0026gt;同理，但这个都适用。 git switch -c \u0026lt;branch\u0026gt;创建并切换指定分支\ngit branch git branch显示本地repo的所有分⽀。\n-v显示详细信息 带*的为当前分支 git branch -r显示远程仓库的所以分支。\n可以使用git checkout \u0026lt;branch\u0026gt;检出远程分支，可以省略origin/。 git branch -a显示本地和远程的所有分支\ngit branch -m \u0026lt;old_branch_name\u0026gt; \u0026lt;new_branch_name\u0026gt;重命名分支\n新分支名已经存在， -M 强制重命名。 git branch \u0026lt;name\u0026gt;创建指定分支\ngit branch -D \u0026lt;branch\u0026gt;强制删除指定分支，无论是否合并到当前分支。\ngit branch -d \u0026lt;branch\u0026gt;删除指定的分支，如果没有合并到当前分支，git会阻止操作。\ngit merge git merge \u0026lt;branch\u0026gt;合并指定分支。将指定\u0026lt;branch\u0026gt;分⽀合并到当前分⽀。 是在当前分支合并指定分支。 合并分支可能会出现冲突。要解决冲突之后才能合并。 git merge --abort放弃本次合并 git一般命令 git revert git revert \u0026lt;commit\u0026gt; 对指定\u0026lt;commit\u0026gt;创建⼀个undo的commit，并应⽤到当前分⽀。就是撤销指定的提交并保留记录 效果：撤销指定的提交，回到了撤销提交的是上个版本，保留了撤销历史。会打开编辑器显示具体效果 一般不用 git reset git reset \u0026lt;commit\u0026gt;重置到指定的提交，不会保留commit历史。工作区和暂存区会变成未跟踪。--hard选项完全重置到指定提交。未跟踪的重置不了。重置历史可以通过git reflog查看，利用这个可以重置已经重置的版本库。\n\u0026lt;commit\u0026gt;可以是： HEAD表示最新的提交或者这个版本库，HEAD^、HEAD~1上上次提交或者上个版本 或者使用commit_hash，提交的哈希值可以使用git log查看，只需要前几位就行。 git reset（重置到最新的提交）移除所有暂存区、工作区的修改，到未跟踪。这些命令其实省略了HEAD\ngit reset --hard 重置到最新的提交，删除工作区和暂存区\ngit reset \u0026lt;file\u0026gt;将\u0026lt;file\u0026gt;从暂存区移除，但保持⼯作区不变。此操作不会修改⼯作区的任何⽂件。\ngit restore git restore \u0026lt;file\u0026gt;...撤销对工作区的修改，是对以跟踪的文件当未添加到暂存区的文件。多个文件用空格分开。 git checkout -- \u0026lt;file\u0026gt;...同理，--可以省略 git restore --staged \u0026lt;file\u0026gt;...撤销对暂存区的修改到未跟踪。针对添加到暂存区的文件。 git reset HEAD \u0026lt;file\u0026gt;...同理，HEAD可以省略。 具体用哪一个，git都会有提示，不用记。 git checkout git checkout \u0026lt;branch\u0026gt;切换到指定的分支\n如果分支为远程分支，则检出远程分支 git checkout -b \u0026lt;new-branch\u0026gt;切换并创建指定的分支\ngit checkout \u0026lt;file\u0026gt;撤销工作区的修改\ngit restore \u0026lt;file\u0026gt;同理 git checkout \u0026lt;commit\u0026gt;根据指定的提交创建一个分支,处于游离态。一般不用。\ngit checkout -b \u0026lt;local_branch_name\u0026gt; origin/\u0026lt;remote_branch_name\u0026gt;切换到远程分支\ngit checkout \u0026lt;branch\u0026gt;差不多，可以使用git fetch origin获取仓库所以信息，在检出分支。 git checkout -切换到前一个分支。\ngit remote 用来管理远程仓库列表，origin为远程仓库的默认别名。这些远端仓库的信息都被保存在./git/config 文件中。\ngit remote列出所有已配置的远程仓库的信息。\n-v显示详细信息 git remote add \u0026lt;remote_name\u0026gt; \u0026lt;remote_url\u0026gt;添加远程仓库\n添加⼀个新的远程连接。添加后可使⽤ \u0026lt;name\u0026gt;作为指定\u0026lt;url\u0026gt;远程连接的名称。 只有配置了这个才能推送到远程仓库。 git remote rename \u0026lt;old_name\u0026gt; \u0026lt;new_name\u0026gt;重命名远程仓库。 git remote set-url \u0026lt;remote_name\u0026gt; \u0026lt;new_url\u0026gt;修改远程仓库的url。 git remote remove \u0026lt;remote_name\u0026gt;或git remote rm \u0026lt;remote_name\u0026gt;删除远程仓库。 git remote show \u0026lt;remote_name\u0026gt;显示远程仓库的详细信息，包括 URL、跟踪的分支等。\n补充如何创建远程仓库\n创建远程仓库可以先在github上创建好，然后在本地pull下来，在进行修改后push上去。\n可以建一个空白仓库，在本地push上去，但需要进行绑定。\n1 2 3 git remote add origin https://github.com/username/null-project.git git branch -M main git push -u origin main gjit push 将本地仓库推送到远程仓库\ngit push \u0026lt;remote_repository\u0026gt; \u0026lt;本地分支名\u0026gt;:\u0026lt;远程分支名\u0026gt;推送本地分支到指定的远程分支。如果远程分支不存在，会自动创建。:前后不能有空格。\n当分支同名，可以简写成git push \u0026lt;remote_repository\u0026gt; \u0026lt;本地分支名\u0026gt; 果无法提交的话执行，-f、--force选项强制推送，一般不用。 git push -u \u0026lt;remote_repository\u0026gt; \u0026lt;本地分支名\u0026gt;设置默认推送分支。\n作用：这样设置以后，推送到远程仓库可以简写成git push git push 代替 git push origin master -u是--set-upstream的短形式。 git push \u0026lt;remote_repository\u0026gt; -d \u0026lt;远程分支名\u0026gt;删除远程分支 。\n--delete长选项。 git push origin :test同理，没有写本地分支，就是删除远程分支。 git push \u0026lt;remote\u0026gt; \u0026lt;tagname\u0026gt;推送指定标签到指定远程仓库，一般为`origin``\n``git push \u0026ndash;tags`推送所用标签到远程\ngit pull git pull \u0026lt;remote_repository\u0026gt; \u0026lt;远程分支名\u0026gt;:\u0026lt;本地分支名\u0026gt;从远程仓库拉取最新代码到本地仓库。 git pull会拉取并合并，出现冲突要解决之后才能合并。 git fetch获取当前远程仓库的最新信息，不会合并。 通常可以简写成git pull,远程仓库默认是origin，分支默认是当前分支。 git pull --rebase\u0026lt;remote\u0026gt; 抓取远程分⽀，并以rebase模式并⼊本地repo⽽不是merge。 git fetch git fetch origin获取远程仓库最新的更改。不会合并。默认仓库是origin，分支是当前分支，这里可以省略origin git fetch origin \u0026lt;branch\u0026gt;获取特定分支的更改. git fetch --all获取所用仓库远程仓库的最新更改。 与git pull的区别 都会获取远程仓库最新的更改。 但是fetch不会合并，而pull会合并。可以理解为git pull 是 git fetch 和 git merge 的组合 获取最新更改之后可以： git checkout \u0026lt;branch\u0026gt;检出指定分支，如果加origin要这样git checkout -b \u0026lt;branch\u0026gt; origin/\u0026lt;branch\u0026gt; git merge origin/master合并远程 master 分支的更改到当前的分支 git rebase origin/master使用 rebase 来整合更改（这可能会改变提交历史） git stash git stash保存工作区、暂存区，可以切换分支去完成别的任务。不保存修改，未提交的修改会错乱到别的分支。并且只能保存已追踪的文件。 git stash list查看保存的工作区以及暂存区。 git stash apply恢复保存的工作区以及暂存区。 这个命令执行之后不会删除存储的工作区以及暂存区。 要用git stash drop才能删除。 git stash pop恢复并删除保存的工作区以及暂存区。 默认都是保存、恢复第一个stash即stash@{0}。若要指定第几个在后面加stash@{num}。 如恢复第二个stash：git stash pop stash@{1} 每个分支共用一个stash。 git tag 作用：用于标记项目的版本发布或重要的里程碑。 分类 git tag \u0026lt;tagname\u0026gt; \u0026lt;commit ID\u0026gt;轻量标签 git tag vn.n.n打标签，n.n.n表示对应的版本号，版本号前面一般加v，遵循一定的命名规范，如v1.0.1。 默认是打在最新的一次提交。 后面跟提交的哈希值可以指定给那次提交打标签。如git tag v0.9.0 f52c633。 哈希值可以通过git log查看 git tag -a \u0026lt;tagname\u0026gt; -m \u0026quot;\u0026lt;tag message\u0026gt;\u0026quot; \u0026lt;commit ID\u0026gt;附注标签 如git tag -a v0.1 -m \u0026quot;version 0.1 released\u0026quot; 1094adb 推荐id省略默认最新提交。 查看标签 git tag显示所有的本地tag列表，按照字母顺序排序。如果tag数量较多，可能会显示不全。省略选项-l、--list git show \u0026lt;tagname\u0026gt;显示指定tag的详细信息，包括提交的作者、提交时间、提交信息等。 git tag -n：显示tag列表，并同时显示每个tag对应的提交信息。 git ls-remote --tags origin：显示远程仓库中的所有tag信息。更推荐这种。 或者先git fetch获取最新的更改，然后git tag检出所有标签。 删除标签 git tag -d \u0026lt;tagname\u0026gt;删除本地标签 删除远程标签：首先需要在本地删除标签，然后推送到远程仓库 git push origin :refs/tags/\u0026lt;tagname\u0026gt; 远程标签是refs/tags/v0.0.1这样存在的，跟删除远程分支差不多。 标签一旦创建，就不能直接修改,如果需要修改标签，通常需要删除原标签，并重新创建一个新标签。 推送标签 打的标签不会自动推送到远程仓库，需要手动推送。 git push \u0026lt;remote\u0026gt; \u0026lt;tagname\u0026gt;推送指定标签到指定远程仓库，一般为origin git push \u0026lt;remote\u0026gt; --tags推送所用标签到远程 git复杂命令 git log git log以缺省格式显示全部commit历史。更多⾃定义参数请参考后续部分。q退出，空格下一页，h查看帮助 git log --stat：显示详细的commit历史。 git log -\u0026lt;limit\u0026gt;限制log的显示数量。例如：”git log -5”仅显示最新5条commit。 git log --oneline每⾏显示⼀条commit，简化信息。与--pretty=oneline等效 git log --author= \u0026quot;\u0026lt;pattern\u0026gt;\u0026quot;按提交者名字搜索并显示commit。 git log --grep= \u0026quot;\u0026lt;pattern\u0026gt;\u0026quot;按指定内容搜索并显示commit。 git log \u0026lt;since\u0026gt;..\u0026lt;until\u0026gt;显示指定范围的commit。范围参数可以是commit ID、分⽀名称、HEAD或任意相对位置。 git log -- \u0026lt;file\u0026gt;仅显示包含指定⽂件修改的commit。 git log --graph使⽤\u0026ndash;graph参数显示图形化的branch信息。 git diff git diff⽐较⼯作区和暂存区的修改。 git diff HEAD⽐较⼯作区和上⼀次commit后的修改。 HEAD指向当前分支最新的commit版本库 git diff --cached⽐较暂存区和上⼀次commit后的修改。 git diff --stashed查看暂存区与最新提交的差异，与上面一样 git diff \u0026lt;commit1\u0026gt; \u0026lt;commit2\u0026gt;查看两个提交之间的差异。 git diff \u0026lt;filename\u0026gt;后面指定文件，只查看该文件的修改情况，没有参数查询全部 用git diff HEAD -- readme.txt命令可以查看版本库和工作区里面最新版本的区别 git config 作用：通过git config命令配置git的配置文件\ngit配置文件级别分为：\n仓库级别 --local 【优先级最高】。文件所在位置仓库下的.git/config 当前用户级别 --global【优先级次之】一般配置它。文件所在位置用户家目录下的.gitconfig 系统所有用户级别 --system【优先级最低】。文件所在位置git安装目录下的 ./etc/gitconfig -l、--list查看配置。常用\ngit config -l查看所有的配置信息，依次是系统级别、用户级别、仓库级别 git config --local -l 查看仓库级别配置。必须要进入到具体的目录下。 git config --global -l 查看当前用户配置 git config --system -l 查看系统所有用户配置 可以与--show-origin 显示文件位置，--show-scope显示文件级别组合使用 -e、--edit打开编辑器编辑指定级别的配置文件，没有指定默认仓库级别，会使用默认编辑器打开编辑。安装的时候设置的。\n添加配置、修改配置：直接配置对应的配置参数就行。一般配置用户级别就行。省略了--add选项。没有指定级别，默认仓库基本。常用的添加配置命令:\n用户邮箱和用户名。安装git之后必设置的配置\ngit config --global user.email \u0026quot;Your mail\u0026quot;\ngit config --global user.name \u0026quot;Your name\u0026quot;\n如果我们没有配置，在提交代码时会有如下错误：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 xxx@DESKTOP-MD21325 MINGW64 /d/test/test (master) $ git commit -m \u0026#34;feature: add readme\u0026#34; Author identity unknown *** Please tell me who you are. Run git config --global user.email \u0026#34;you@example.com\u0026#34; git config --global user.name \u0026#34;Your Name\u0026#34; to set your account\u0026#39;s default identity. Omit --global to set the identity only in this repository. fatal: unable to auto-detect email address (got \u0026#39;xxx@DESKTOP-MD21325.(none)\u0026#39;) 设置自己的代理。网速慢必设置的配置\ngit config --global http.proxy \u0026quot;http://proxy_ip:port\u0026quot;\ngit config --global https.proxy \u0026quot;https://proxy_ip:port\u0026quot;\n--unset取消配置，注意要指定取消的配置级别。常用取消配置命令：\n取消代理配置\ngit config --global --unset http.proxy\ngit config --global --unset https.proxy\n--get查看指定级别、指定配置项的配置，默认仓库级别。如：\n查看代理配置\ngit config --global --get http.proxy\ngit config --global --get https.proxy\ngit config --global alias.\u0026lt;alias-name\u0026gt; \u0026lt;git-command\u0026gt;配置⼀个git命令的快捷⽅式。例如：配置”alias.glog log \u0026ndash;graph \u0026ndash;oneline”使”git glog”相当于”git log \u0026ndash;graph \u0026ndash;oneline”.\ngit config --global core.editor \u0026lt;editor\u0026gt;配置⽂本编辑器，例如vi，在必要时⾃动打开此⽂本编辑器。安装的时候也可以指定默认编辑器。\ngit submodule git submodule init初始化子模块，将子模块的配置信息存储在父仓库中。\n通常执行之后再执行更新子模块使用，如克隆带有子模块的仓库，没有加--recursive，是不会克隆子模块的 初始化子模块之后，执行更新子模块就会根据配置信息下载子模块 git submodule update根据父仓库子模块的配置信息更新子模块，如果没有初始化子模块可以加参数--init，会下载与父项目绑定版本的子模块，若要更新加--remote\n--init初识化子模块 --recursive会递归下载子模块的子模块 --remote根据子模块远程仓库的配置信息更新子模块，会下载最新版本的子模块 注意更新之后要提交更新的版本，否则当在执行更新命令没有加--remote时会退回与父仓库绑定的版本 git submodule add \u0026lt;repository\u0026gt; \u0026lt;path\u0026gt;添加子模块。其中，\u0026lt;repository\u0026gt;是子模块的远程仓库地址，\u0026lt;path\u0026gt;是子模块在主项目中的路径。\n子模块可以当正常仓库使用。创建时\u0026lt;path\u0026gt;路径不能存在文件，更克隆差不多。 git subtree 作用：将一个仓库中的目录作为另一个仓库，可以指定分支 用途：搭建项目网站时，将项目网站资源推送到gh-pages分支上 git subtree push --prefix=dist origin gh-pages将目录添加到gh-pages分支上，dist为项目网站的目录 git subtree push --prefix=\u0026lt;prefix\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;branch\u0026gt;将子目录的内容推送到远程仓库。它会将当前仓库中子目录的修改推送到指定的远程仓库和分支中。 注意：以这种推送的方式添加的subtree不能执行subtree pull命令，只有通过subtree add添加的才能都执行，但能够执行subtree push命令 git subtree pull --prefix=\u0026lt;prefix\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;branch\u0026gt;这个命令用于从远程仓库更新子目录的内容。它会拉取远程仓库的最新代码，并更新到当前仓库的子目录中。 git subtree add --prefix=\u0026lt;prefix\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;branch\u0026gt;这个命令用于将远程仓库的内容作为子目录添加到当前仓库中。\u0026lt;prefix\u0026gt;是子目录的名称，\u0026lt;repository\u0026gt;是远程仓库的地址，\u0026lt;branch\u0026gt;是要合并的分支。 git rebase 作用：rebase翻译成变基，顾名思义：改变基准点。可以使提交历史更加清晰和线性。\n原因：通过合并两个不同的分支，提交历史会很错乱。而通过变基，会使得提交历史更加整洁和可读。\n如何实现：就是修改创建分支的起点（基准点），到最新的提交。起点变了，提交历史就简化了。\n命令：\ngit rebase \u0026lt;base\u0026gt;基于\u0026lt;base\u0026gt;对当前分⽀进⾏rebase。\u0026lt;base\u0026gt;可以是commit、分⽀名称、tag或相对于HEAD的commit。 git rebase -i \u0026lt;base\u0026gt;以交互模式对当前分⽀做rebase。 rebase的过程中可能会出现冲突，解决冲突之后需要使用git add命令将解决冲突后的文件标记为已解决，然后，使用git rebase --continue命令继续rebase过程。Git会尝试继续应用剩余的提交。如果再次出现冲突，你需要重复上述解决冲突和继续rebase的步骤。 如果在rebase过程中出现了问题，或者你决定放弃rebase操作，你可以使用git rebase --abort命令来撤销整个rebase操作。 git rebase的注意事项\n避免对已经推送到远程仓库的提交执行rebase操作：这可能会导致提交历史的不一致，给其他协作者带来困扰。 保持工作目录干净：在执行rebase之前，确保你的工作目录中没有未提交的更改。 谨慎使用：由于rebase会改变提交历史，因此在与他人共享分支时要特别小心。通常，在公共分支上应该使用merge而不是rebase。 通过掌握git rebase的用法和注意事项，你可以更有效地管理你的Git仓库，保持代码的清晰和整洁。\n​\n扩展 .gitignore文件 Git提供了.gitignore文件，用于指定哪些文件或目录应该被Git忽略，不纳入版本控制系统中。.gitignore文件是一个文本文件，可以包含一些简单的规则，指定应该忽略哪些文件或目录。以下是一些.gitignore文件的示例规则：\n忽略所有以.tmp结尾的文件：\n1 *.tmp 忽略所有的log文件：\n1 *.log 忽略所有的.idea目录：\n1 .idea/ 忽略所有的build目录及其内容：\n1 build/ 忽略根目录下的config.json文件，但不忽略子目录中的config.json文件：\n1 /config.json 忽略所有的node_modules目录及其内容：\n1 node_modules/ 忽略所有的DS_Store文件（Mac OS X系统中的文件）：\n1 .DS_Store 可以将这些规则写入.gitignore文件中，并将该文件添加到Git仓库中，以使Git忽略这些文件或目录。需要注意的是，即使某些文件或目录已经被添加到Git仓库中，也可以通过修改.gitignore文件来让Git忽略它们，但需要执行以下命令才能使.gitignore文件生效：\n1 2 3 4 git rm -r --cached . git add . git commit -m \u0026#34;update .gitignore\u0026#34; git push 这些命令会删除Git缓存中已经添加的文件，然后重新添加文件并提交更改，以使.gitignore文件生效。\n总结：\n当Git执行提交操作时，它会检查.gitignore文件中列出的文件和目录，并将它们从提交中排除。这是非常有用的，因为有些文件或目录不应该被纳入版本控制系统中，例如编译生成的文件、日志文件、临时文件等。\n.gitignore文件的语法是基于模式匹配的，其中的特殊字符有：\n*：匹配任意字符，但不包括路径分隔符（/）。 ?：匹配任意单个字符，但不包括路径分隔符（/）。 /：路径分隔符，用于指定目录。 !：用于否定模式，即不忽略指定的文件或目录。 可以在.gitignore文件中使用通配符、路径、注释等语法，以更精确地指定需要忽略的文件或目录。同时，可以在仓库的根目录下创建一个.gitignore文件，也可以在子目录中创建独立的.gitignore文件。\ngit账户认证 当我们对远程仓库就行修改时，需要对应的权限，不是什么人都能够修改仓库。只有通过了git账户认证，才能修改对应的仓库。 常见git账户认证的方式： SSH秘钥认证 这是Git中最常见的认证方式之一。用户首先生成一对公钥和私钥，然后将公钥添加到Git服务器上的用户帐户中。当用户尝试与Git服务器进行通信时，Git将使用私钥进行身份验证。这种方式相对安全，因为私钥是保存在用户本地机器上的，不会被传输到Git服务器。 秘钥生成命令：bash中运行ssh-keygen,一直回车就行，秘钥位置：主目录下的.ssh目录 公钥设置位置：github账户Settings-\u0026gt;SSH and GPG keys-\u0026gt;New SSH key将公钥复制粘贴保存就行。 HTTPS认证 在这种方式中，用户需要提供用户名和密码进行身份验证。用户需要在Git服务器上创建一个用户帐户，并将其关联到本地的Git仓库中。当用户执行需要身份验证的操作时，Git会要求输入用户名和密码。这种方式相对简单，适用于个人项目或小型团队。 设置位置：在使用Git进行操作时，如push或pull，系统会提示你输入用户名和密码进行身份验证。 访问令牌（Personal Access Token）认证：不常用 访问令牌提供了一种更安全、更灵活的身份验证方式，因为它可以限制令牌的使用权限，并且可以随时撤销或重新生成令牌。 设置位置：github账户Settings-\u0026gt;Developer Settings-\u0026gt;Personal access tokens-\u0026gt;Tokens (classic)-\u0026gt;Generate new token，然后根据自己的需求设置token的权限。 ssh-keygen ssh-keygen命令是一个用于生成、管理和转换SSH认证密钥的工具。它支持RSA和DSA两种认证密钥类型，并且提供了多种选项和参数，以满足不同的需求。\n使用ssh-keygen命令，你可以生成新的密钥对，指定密钥的长度、类型以及保存的文件名。生成的私钥将保存在本地，而公钥则用于在SSH服务器上进行身份验证。\n以下是一些常用的ssh-keygen命令选项：\n-t：指定要创建的密钥类型，默认为RSA。 -b：指定密钥长度（以位为单位）。对于RSA密钥，最小要求是768位，默认是2048位。对于DSA密钥，长度必须是1024位（根据FIPS 1862标准规定）。 -f：指定用于保存密钥的文件名。如果不指定，将使用默认值id_rsa（对于私钥）和id_rsa.pub（对于公钥）。 -C：提供一个新注释，通常用于标识密钥的用途或所有者。 -P 和 -N：分别用于提供旧密码和新密码，以保护私钥文件。如果留空，则表示不需要密码。 在生成密钥对后，你可以将公钥复制到需要访问的SSH服务器上，通常是将公钥内容追加到服务器的~/.ssh/authorized_keys文件中。这样，当你使用SSH客户端连接到服务器时，客户端将使用私钥进行身份验证，如果验证成功，你将能够无需输入密码即可登录到服务器。\n请注意，私钥的安全性至关重要。私钥应该妥善保管，并且不应该与其他人共享。同时，定期更换密钥对也是保持安全性的好习惯。\n除了生成和管理密钥对，ssh-keygen还提供了其他功能，如转换密钥格式、读取密钥文件等。你可以通过查看ssh-keygen的帮助文档或手册页（通过运行man ssh-keygen命令）来获取更详细的信息和用法示例。\n","date":"2024-04-22T22:18:17+08:00","permalink":"https://arlettebrook.github.io/p/git%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/","title":"Git常用命令"},{"content":"简介 flag用于解析命令行选项。有过类 Unix 系统使用经验的童鞋对命令行选项应该不陌生。例如命令ls -al列出当前目录下所有文件和目录的详细信息，其中-al就是命令行选项。\n命令行选项在实际开发中很常用，特别是在写工具的时候。\n指定配置文件的路径，如redis-server ./redis.conf以当前目录下的配置文件redis.conf启动 Redis 服务器； 自定义某些参数，如python -m SimpleHTTPServer 8080启动一个 HTTP 服务器，监听 8080 端口。如果不指定，则默认监听 8000 端口。 快速使用 学习一个库的第一步当然是使用它。我们先看看flag库的基本使用：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;flag\u0026#34; ) var ( intflag int boolflag bool stringflag string ) func init() { flag.IntVar(\u0026amp;intflag, \u0026#34;intflag\u0026#34;, 0, \u0026#34;int flag value\u0026#34;) flag.BoolVar(\u0026amp;boolflag, \u0026#34;boolflag\u0026#34;, false, \u0026#34;bool flag value\u0026#34;) flag.StringVar(\u0026amp;stringflag, \u0026#34;stringflag\u0026#34;, \u0026#34;default\u0026#34;, \u0026#34;string flag value\u0026#34;) } func main() { flag.Parse() fmt.Println(\u0026#34;int flag:\u0026#34;, intflag) fmt.Println(\u0026#34;bool flag:\u0026#34;, boolflag) fmt.Println(\u0026#34;string flag:\u0026#34;, stringflag) } 可以先编译程序，然后运行（我使用的是 Win10 + Git Bash）：\n1 2 $ go build -o main.exe main.go $ ./main.exe -intflag 12 -boolflag 1 -stringflag test 输出：\n1 2 3 int flag: 12 bool flag: true string flag: test 如果不设置某个选项，相应变量会取默认值：\n1 $ ./main.exe -intflag 12 -boolflag 1 输出：\n1 2 3 int flag: 12 bool flag: true string flag: default 可以看到没有设置的选项stringflag为默认值default。\n还可以直接使用go run，这个命令会先编译程序生成可执行文件，然后执行该文件，将命令行中的其它选项传给这个程序。\n1 go run main.go -intflag 12 -boolflag 1 可以使用-h显示选项帮助信息：\n1 2 3 4 5 6 7 8 $ ./main.exe -h Usage of D:\\code\\golang\\src\\github.com\\darjun\\cmd\\flag\\main.exe: -boolflag bool flag value -intflag int int flag value -stringflag string string flag value (default \u0026#34;default\u0026#34;) 总结一下，使用flag库的一般步骤：\n定义一些全局变量存储选项的值，如这里的intflag/boolflag/stringflag； 在init方法中使用flag.TypeVar方法定义选项，这里的Type可以为基本类型Int/Uint/Float64/Bool，还可以是时间间隔time.Duration。定义时传入变量的地址、选项名、默认值和帮助信息； 在main方法中调用flag.Parse从os.Args[1:]中解析选项。因为os.Args[0]为可执行程序路径，会被剔除。 注意点：\nflag.Parse方法必须在所有选项都定义之后调用，且flag.Parse调用之后不能再定义选项。如果按照前面的步骤，基本不会出现问题。 因为init在所有代码之前执行，将选项定义都放在init中，main函数中执行flag.Parse时所有选项都已经定义了。\n选项格式 flag库支持三种命令行选项格式。\n1 2 3 -flag -flag=x -flag x -和--都可以使用，它们的作用是一样的。有些库使用-表示短选项，--表示长选项。相对而言，flag使用起来更简单。\n第一种形式只支持布尔类型的选项，出现即为true，不出现为默认值。 第三种形式不支持布尔类型的选项。因为这种形式的布尔选项在类 Unix 系统中可能会出现意想不到的行为。看下面的命令：\n1 cmd -x * 其中，*是 shell 通配符。如果有名字为 0、false的文件，布尔选项-x将会取false。反之，布尔选项-x将会取true。而且这个选项消耗了一个参数。 如果要显示设置一个布尔选项为false，只能使用-flag=false这种形式。\n遇到第一个非选项参数（即不是以-和--开头的）或终止符--，解析停止。运行下面程序：\n1 $ ./main.exe noflag -intflag 12 将会输出：\n1 2 3 int flag: 0 bool flag: false string flag: default 因为解析遇到noflag就停止了，后面的选项-intflag没有被解析到。所以所有选项都取的默认值。\n运行下面的程序：\n1 $ ./main.exe -intflag 12 -- -boolflag=true 将会输出：\n1 2 3 int flag: 12 bool flag: false string flag: default 首先解析了选项intflag，设置其值为 12。遇到--后解析终止了，后面的--boolflag=true没有被解析到，所以boolflag选项取默认值false。\n解析终止之后如果还有命令行参数，flag库会存储下来，通过flag.Args方法返回这些参数的切片。 可以通过flag.NArg方法获取未解析的参数数量，flag.Arg(i)访问位置i（从 0 开始）上的参数。 选项个数也可以通过调用flag.NFlag方法获取。\n稍稍修改一下上面的程序：\n1 2 3 4 5 6 7 8 9 10 11 func main() { flag.Parse() fmt.Println(flag.Args()) fmt.Println(\u0026#34;Non-Flag Argument Count:\u0026#34;, flag.NArg()) for i := 0; i \u0026lt; flag.NArg(); i++ { fmt.Printf(\u0026#34;Argument %d: %s\\n\u0026#34;, i, flag.Arg(i)) } fmt.Println(\u0026#34;Flag Count:\u0026#34;, flag.NFlag()) } 编译运行该程序：\n1 2 $ go build -o main.exe main.go $ ./main.exe -intflag 12 -- -stringflag test 输出：\n1 2 3 4 [-stringflag test] Non-Flag Argument Count: 2 Argument 0: -stringflag Argument 1: test 解析遇到--终止后，剩余参数-stringflag test保存在flag中，可以通过Args/NArg/Arg等方法访问。\n整数选项值可以接受 1234（十进制）、0664（八进制）和 0x1234（十六进制）的形式，并且可以是负数。实际上flag在内部使用strconv.ParseInt方法将字符串解析成int。 所以理论上，ParseInt接受的格式都可以。\n布尔类型的选项值可以为：\n取值为true的：1、t、T、true、TRUE、True； 取值为false的：0、f、F、false、FALSE、False。 另一种定义选项的方式 上面我们介绍了使用flag.TypeVar定义选项，这种方式需要我们先定义变量，然后变量的地址。 还有一种方式，调用flag.Type（其中Type可以为Int/Uint/Bool/Float64/String/Duration等）会自动为我们分配变量，返回该变量的地址。用法与前一种方式类似：\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 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;flag\u0026#34; ) var ( intflag *int boolflag *bool stringflag *string ) func init() { intflag = flag.Int(\u0026#34;intflag\u0026#34;, 0, \u0026#34;int flag value\u0026#34;) boolflag = flag.Bool(\u0026#34;boolflag\u0026#34;, false, \u0026#34;bool flag value\u0026#34;) stringflag = flag.String(\u0026#34;stringflag\u0026#34;, \u0026#34;default\u0026#34;, \u0026#34;string flag value\u0026#34;) } func main() { flag.Parse() fmt.Println(\u0026#34;int flag:\u0026#34;, *intflag) fmt.Println(\u0026#34;bool flag:\u0026#34;, *boolflag) fmt.Println(\u0026#34;string flag:\u0026#34;, *stringflag) } 编译并运行程序：\n1 2 $ go build -o main.exe main.go $ ./main.exe -intflag 12 将输出：\n1 2 3 int flag: 12 bool flag: false string flag: default 除了使用时需要解引用，其它与前一种方式基本相同。\n高级用法 定义短选项 flag库并没有显示支持短选项，但是可以通过给某个相同的变量设置不同的选项来实现。即两个选项共享同一个变量。 由于初始化顺序不确定，必须保证它们拥有相同的默认值。否则不传该选项时，行为是不确定的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;flag\u0026#34; ) var logLevel string func init() { const ( defaultLogLevel = \u0026#34;DEBUG\u0026#34; usage = \u0026#34;set log level value\u0026#34; ) flag.StringVar(\u0026amp;logLevel, \u0026#34;log_type\u0026#34;, defaultLogLevel, usage) flag.StringVar(\u0026amp;logLevel, \u0026#34;l\u0026#34;, defaultLogLevel, usage + \u0026#34;(shorthand)\u0026#34;) } func main() { flag.Parse() fmt.Println(\u0026#34;log level:\u0026#34;, logLevel) } 编译、运行程序：\n1 2 3 $ go build -o main.exe main.go $ ./main.exe -log_type WARNING $ ./main.exe -l WARNING 使用长、短选项均输出：\n1 log level: WARNING 不传入该选项，输出默认值：\n1 2 $ ./main.exe log level: DEBUG 解析时间间隔 除了能使用基本类型作为选项，flag库还支持time.Duration类型，即时间间隔。时间间隔支持的格式非常之多，例如\u0026quot;300ms\u0026quot;、\u0026quot;-1.5h\u0026quot;、“2h45m\u0026quot;等等等等。 时间单位可以是 ns/us/ms/s/m/h/day 等。实际上flag内部会调用time.ParseDuration。具体支持的格式可以参见time（需fq）库的文档。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package main import ( \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) var ( period time.Duration ) func init() { flag.DurationVar(\u0026amp;period, \u0026#34;period\u0026#34;, 1*time.Second, \u0026#34;sleep period\u0026#34;) } func main() { flag.Parse() fmt.Printf(\u0026#34;Sleeping for %v...\u0026#34;, period) time.Sleep(period) fmt.Println() } 根据传入的命令行选项period，程序睡眠相应的时间，默认 1 秒。编译、运行程序：\n1 2 3 4 5 6 $ go build -o main.exe main.go $ ./main.exe Sleeping for 1s... $ ./main.exe -period 1m30s Sleeping for 1m30s... 自定义选项 除了使用flag库提供的选项类型，我们还可以自定义选项类型。我们分析一下标准库中提供的案例：\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 package main import ( \u0026#34;errors\u0026#34; \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;time\u0026#34; ) type interval []time.Duration func (i *interval) String() string { return fmt.Sprint(*i) } func (i *interval) Set(value string) error { if len(*i) \u0026gt; 0 { return errors.New(\u0026#34;interval flag already set\u0026#34;) } for _, dt := range strings.Split(value, \u0026#34;,\u0026#34;) { duration, err := time.ParseDuration(dt) if err != nil { return err } *i = append(*i, duration) } return nil } var ( intervalFlag interval ) func init() { flag.Var(\u0026amp;intervalFlag, \u0026#34;deltaT\u0026#34;, \u0026#34;comma-seperated list of intervals to use between events\u0026#34;) } func main() { flag.Parse() fmt.Println(intervalFlag) } 首先定义一个新类型，这里定义类型interval。\n新类型必须实现flag.Value接口：\n1 2 3 4 5 // src/flag/flag.go type Value interface { String() string Set(string) error } 其中String方法格式化该类型的值，flag.Parse方法在执行时遇到自定义类型的选项会将选项值作为参数调用该类型变量的Set方法。 这里将以,分隔的时间间隔解析出来存入一个切片中。\n自定义类型选项的定义必须使用flag.Var方法。\n编译、执行程序：\n1 2 3 4 5 $ go build -o main.exe main.go $ ./main.exe -deltaT 30s [30s] $ ./main.exe -deltaT 30s,1m,1m30s [30s 1m0s 1m30s] 如果指定的选项值非法，Set方法返回一个error类型的值，Parse执行终止，打印错误和使用帮助。\n1 2 3 4 5 $ ./main.exe -deltaT 30x invalid value \u0026#34;30x\u0026#34; for flag -deltaT: time: unknown unit x in duration 30x Usage of D:\\code\\golang\\src\\github.com\\darjun\\go-daily-lib\\flag\\self-defined\\main.exe: -deltaT value comma-seperated list of intervals to use between events 解析程序中的字符串 有时候选项并不是通过命令行传递的。例如，从配置表中读取或程序生成的。这时候可以使用flag.FlagSet结构的相关方法来解析这些选项。\n实际上，我们前面调用的flag库的方法，都会间接调用FlagSet结构的方法。flag库中定义了一个FlagSet类型的全局变量CommandLine专门用于解析命令行选项。 前面调用的flag库的方法只是为了提供便利，它们内部都是调用的CommandLine的相应方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // src/flag/flag.go var CommandLine = NewFlagSet(os.Args[0], ExitOnError) func Parse() { CommandLine.Parse(os.Args[1:]) } func IntVar(p *int, name string, value int, usage string) { CommandLine.Var(newIntValue(value, p), name, usage) } func Int(name string, value int, usage string) *int { return CommandLine.Int(name, value, usage) } func NFlag() int { return len(CommandLine.actual) } func Arg(i int) string { return CommandLine.Arg(i) } func NArg() int { return len(CommandLine.args) } 同样的，我们也可以自己创建FlagSet类型变量来解析选项。\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 package main import ( \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { args := []string{\u0026#34;-intflag\u0026#34;, \u0026#34;12\u0026#34;, \u0026#34;-stringflag\u0026#34;, \u0026#34;test\u0026#34;} var intflag int var boolflag bool var stringflag string fs := flag.NewFlagSet(\u0026#34;MyFlagSet\u0026#34;, flag.ContinueOnError) fs.IntVar(\u0026amp;intflag, \u0026#34;intflag\u0026#34;, 0, \u0026#34;int flag value\u0026#34;) fs.BoolVar(\u0026amp;boolflag, \u0026#34;boolflag\u0026#34;, false, \u0026#34;bool flag value\u0026#34;) fs.StringVar(\u0026amp;stringflag, \u0026#34;stringflag\u0026#34;, \u0026#34;default\u0026#34;, \u0026#34;string flag value\u0026#34;) fs.Parse(args) fmt.Println(\u0026#34;int flag:\u0026#34;, intflag) fmt.Println(\u0026#34;bool flag:\u0026#34;, boolflag) fmt.Println(\u0026#34;string flag:\u0026#34;, stringflag) } NewFlagSet方法有两个参数，第一个参数是程序名称，输出帮助或出错时会显示该信息。第二个参数是解析出错时如何处理，有几个选项：\nContinueOnError：发生错误后继续解析，CommandLine就是使用这个选项； ExitOnError：出错时调用os.Exit(2)退出程序； PanicOnError：出错时产生 panic。 随便看一眼flag库中的相关代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // src/flag/flag.go func (f *FlagSet) Parse(arguments []string) error { f.parsed = true f.args = arguments for { seen, err := f.parseOne() if seen { continue } if err == nil { break } switch f.errorHandling { case ContinueOnError: return err case ExitOnError: os.Exit(2) case PanicOnError: panic(err) } } return nil } 与直接使用flag库的方法有一点不同，FlagSet调用Parse方法时需要显示传入字符串切片作为参数。因为flag.Parse在内部调用了CommandLine.Parse(os.Args[1:])。 示例代码都放在GitHub上了。\n参考 flag库文档 Go 每日一库之 flag 深入探究 Go flag 标准库附源码分析 ","date":"2024-04-22T18:02:17+08:00","permalink":"https://arlettebrook.github.io/p/go-flag%E5%BA%93%E4%BB%8B%E7%BB%8D/","title":"Go flag库介绍"},{"content":"静态网页生成器 无论您需要搭建个人博客还是为您的项目创建文档，静态网页生成器（static site generator）都是一个不错的选择。无需服务器、数据库，只要你熟悉 Markdown，喜欢GitHub，使用生成器创建静态 HTML 文件，然后推送到 GitHub Pages 等免费服务即可。\n常见的静态网页生成器 Hugo是由Go语言实现的静态网站生成器。简单、易用、高效、易扩展、快速部署。 jekyll 是一个静态网页、博客生成器 vuepress是基于 Vue 的静态网页生成器 Hexo 是一个由Nodejs驱动的快速、简洁且高效的博客框架。Hexo 使用 Markdown（或其他渲染引擎）解析文章，在几秒内，即可利用靓丽的主题生成静态网页。 参考：静态网页生成器\n快速使用Hugo搭建个人博客站点 安装Hugo 根据自己的操作系统，下载已经构建好的Hugo二进制文件官方地址\n官方推荐下载扩展版，支持的功能更多 解压之后，将hugo可执行文件加入到PATH环境变量中，即可使用\n1 2 3 hugo version # 查看版本，扩展版含这个extended标签 hugo -h # 显示帮助信息 hugo subcommand -h # 获取子命令的帮助信息 使用Hugo hugo需要配合git一起使用，并且官方推荐使用bash作为终端\n创建项目并安装主题hugo-theme-stack\n1 2 3 4 5 hugo new site quickstart # 创建目录结构 cd quickstart git init git submodule add https://github.com/CaiJimmy/hugo-theme-stack/ themes/hugo-theme-stack # 安装主题 添加内容并使用安装主题的默认配置\n使用安装主题的实例进行快速添加内容 只需要进入主题文件中的exampleSite中的content拷贝到quickstart根目录中 同理，在主题文件中的exampleSite中的hugo.yaml拷贝到quickstart根目录中，重命名为,并删除hugo.toml 最后运行\n1 2 hugo server # 本地启动一个http服务器，便于开发和测试站点，默认热更新 hugo server --navigateToChanged # 自动重定向：编辑内容时，浏览器会自动重定向到上次修改的页面 会使用到的命令\n1 2 3 4 5 6 7 8 9 10 11 hugo new content post/fist-post.md # 会在content目录下创建post/fist-post.md文件 # 执行完后，会在content/post目录自动生成一个MarkDown格式的first.md文件： +++ date = \u0026#34;2015-01-08T08:36:54-07:00\u0026#34; draft = true title = \u0026#34;Fist Post\u0026#34; +++ # draft 默认为true，构建网站时不会构建该文档 # 要构建草稿文档可以用-D或--buildDrafts选项启动服务 hugo server -D # title 默认为文件名首字母大写 构建命令\n进入项目目录，运行\n1 hugo hugo命令会构建生成静态文件，会将文件发布项目的public目录下\n要将站点发布到其他目录，请使用该标志--destination或在站点配置中设置publishDir\n注意：每次构建不会清空public目录，只会覆盖旧内容。\n这样做是为了防止，构建之后在public添加的文件被删除\n草稿、未来和过期内容\nHugo 允许在内容的前面设置draft、date、publishDate和expiryDate。默认情况下，Hugo 在以下情况下不会发布内容：\n其draft值为true\n是date在未来\n是publishDate在未来\n已经expiryDate过去了\n下面的行为可以取消\n1 2 3 hugo --buildDrafts # or -D hugo --buildExpired # or -E hugo --buildFuture # or -F 注意：当这样构建之后，需要手动删除不期望构建的文件，在推送站点\n否则当推送到远程会出现意外的内容\n所以建议运行上面的命令之后，前提public中没有手动添加的文件，在构建之前手动清空public目录，防止出现草稿、过期和未来的内容\n最后将public中的所以文件推送到静态网站托管平台即可\n也可以使用自动构建和部署 更多内容参考：\n使用hugo搭建个人博客站点 （1）带着Stack主题入坑Hugo （2）部署你的Hugo博客 （3）Stack主题的自定义 自定义主题添加了assets/scss、layouts/_default/、layouts/index.html,不用了删了就行 目录结构 archetypes目录包含新内容的模板\n目录下的default.md由标记（markdown）和内容格式\n内容格式：\u0026mdash;yaml\u0026mdash;、+++toml+++、{json}\n1 2 3 4 5 --- # +++/{ date: \u0026#39;{{ .Date }}\u0026#39; # yaml draft: true title: \u0026#39;{{ replace .File.ContentBaseName `-` ` ` | title }}\u0026#39; --- # +++/{ 当运行hugo new content post/my-first-post.md命令时会根据default.md创建内容文件\n1 2 3 4 5 --- date: \u0026#34;2023-08-24T11:49:46-07:00\u0026#34; draft: true title: My First Post --- 可以创建新内容的模版\n1 2 3 archetypes/ ├── default.md └── post.md 若运行hugo new content post/my-first-post.md查找模版的顺序 archetypes/post.md archetypes/default.md themes/my-theme/archetypes/post.md themes/my-theme/archetypes/default.md 如果这些都不存在，Hugo 将使用内置的默认原型 assets目录包含通常通过资产管道传递的全局资源，包括图像、CSS、Sass、JavaScript 和 TypeScript 等资源。\nconfig目录包含站点配置，可能分为多个子目录和文件。对于具有最少配置的项目或不需要在不同环境中表现不同的项目，hugo.toml在项目根目录中命名的单个配置文件就足够了\ncontent目录包含构成站点内容的标记文件（通常是 Markdown）和页面资源。\n对应stack主题： post存放发布的文章格式md page存放导航区域的md格式配置 不同的语言结尾用.en.md等表示 根据模版进行修改即可，根据自己的需求，没有的需要自己补充和修改文件内容 categories存放类别的md格式配置 data目录包含增强内容、配置、本地化和导航的数据文件（JSON、TOML、YAML 或 XML）。\ni18n目录包含多语言站点的翻译表。\ncontent目录包含将内容、数据和资源转换为完整网站的模板。\npublic目录包含运行hugo或hugo server命令时生成的已发布网站。 Hugo 根据需要重新创建该目录及其内容\nresources目录包含 Hugo 资产管道的缓存输出，这些输出是在运行hugo或hugo server命令时生成的。默认情况下，此缓存目录包括 CSS 和图像。 Hugo 根据需要重新创建该目录及其内容。\nstatic目录包含在您构建站点时将复制到公共目录的文件。例如：favicon.ico、robots.txt和 验证站点所有权的文件.与assets差不多\nthemes目录包含一个或多个主题，每个主题都位于其自己的子目录中。\n联合文件系统：\n这样理解：安装的主题里面同样有自己站点的目录结构，hugo构建时会将主题里面的文件挂载到站点，优先级是站点的高 配置文件 hugo支持三种配置文件hugo.tomal、hugo.yaml、hugo.json，喜欢用那个就用那个。\n每种文件格式的规范：TOML、YAML和JSON。\n配置文件可以有多个，可以放到config目录下，默认都是使用hugo开头的文件\n指定配置文件构建\n1 2 hugo server --config other.toml hugo --config a.toml,b.yaml,c.json # 可以指定多个，左边的优先级高 更多内容配置参考\n额外的一些关于配置文件的总结\n默认语言修改为zh-cn，意味着index.md表示中文，index.zh-cn.md也表示中文，此时的英文要用index.en.md表示 更多内容参考：\nHugo官方文档 Hugo中文文档 Hugo theme 文章评论 使用Waline，其教程很完整。\n根据Waline教程从头完成到使用Vercel部署完成。\n最后在config.yaml中的waline的serverURL给上你的Vercel服务器地址。\n以及开启评论，最后waline还可以配置评论通知渠道。\n将cloudflare解析的域名绑定到vercel文档 概括：添加一条CNAME记录值为cname.vercel-dns.com，开启代理，将SSL/TLS修改为完全 将cloudflare解析的域名绑定到github-pages文档 概括：添加一条子域，类型CNAME记录值为username.github.io，开启代理，将SSL/TLS修改为完全,username为你的用户名 更多内容请查阅文档 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 comments: enabled: true provider: waline waline: serverURL: url lang: zh-cn pageview: true copyright: false emoji: - https://unpkg.com/@waline/emojis@1.0.1/weibo requiredMeta: - name - email locale: admin: 👻Hi! placeholder: 🎉留下你的脚印... 搜索引擎优化（SEO） 本网站使用Hugo搭建，而且使用的stack主题支持自动生成基于Open Graph协议（OG协议）的标签，此处记录一下如何在Hugo搭建的网站中做搜索引擎优化（SEO）。\n目的：提升网站在搜索引擎中的排名\nOpen Graph（开放图谱）协议，简称OG协议，是Facebook在2010年公布的一项协议，用来标记网页内容。简单来讲，OG协议就是嵌在网页头部的一些标签，这些标签标记了网页的标题、描述等特征，使得网页成为一个“富媒体对象”，可以被其他社交网站引用。\n很多搜索引擎都支持OG协议，在网页中使用OG协议的标签，就更有利于提升我们的网页在搜索引擎中的排名。\nOG协议的标签在网页中通常表示为类似下面所示的格式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;meta property=\u0026#34;og:title\u0026#34; content=\u0026#34;The Rock\u0026#34; /\u0026gt; \u0026lt;meta property=\u0026#34;og:type\u0026#34; content=\u0026#34;video.movie\u0026#34; /\u0026gt; \u0026lt;meta property=\u0026#34;og:url\u0026#34; content=\u0026#34;https://www.imdb.com/title/tt0117500/\u0026#34; /\u0026gt; \u0026lt;meta property=\u0026#34;og:image\u0026#34; content=\u0026#34;https://ia.media-imdb.com/images/rock.jpg\u0026#34; /\u0026gt; \u0026lt;meta property=\u0026#39;og:url\u0026#39; content=\u0026#39;https://arlettebrook.github.io/search/\u0026#39;\u0026gt; \u0026lt;meta property=\u0026#39;og:site_name\u0026#39; content=\u0026#39;Arlettebrook\u0026amp;#39;s blog\u0026#39;\u0026gt; \u0026lt;meta property=\u0026#39;og:type\u0026#39; content=\u0026#39;article\u0026#39;\u0026gt;\u0026lt;meta property=\u0026#39;article:section\u0026#39; content=\u0026#39;P age\u0026#39; /\u0026gt; -\u0026lt;meta name=\u0026#34;twitter:title\u0026#34; content=\u0026#34;搜索\u0026#34;\u0026gt; +\u0026lt;meta name=\u0026#34;twitter:site\u0026#34; content=\u0026#34;@arlettebrook\u0026#34;\u0026gt; + \u0026lt;meta name=\u0026#34;twitter:creator\u0026#34; content=\u0026#34;@arlettebrook\u0026#34;\u0026gt;\u0026lt;meta name=\u0026#34;twitter:title\u0026#34; co ntent=\u0026#34;搜索\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;twitter:description\u0026#34; content=\u0026#34;\u0026#34;\u0026gt;\u0026lt;link rel=\u0026#34;alternate\u0026#34; type=\u0026#34;application/js on\u0026#34; href=\u0026#34;https://arlettebrook.github.io/search/index.json\u0026#34;\u0026gt; \u0026lt;link rel=\u0026#34;shortcut icon\u0026#34; href=\u0026#34;/img/favicon.ico\u0026#34; /\u0026gt; stack主题提供了对OG协议的支持，只需要在网站根目录下的config/_default/params.en.yaml和config/_default/params.zh-cn.yaml配置文件中开启即可：\n1 2 3 4 5 6 7 opengraph: twitter: # Your Twitter username site: arlettebrook # Available values: summary, summary_large_image card: summary_large_image 这样，Hugo在生成和部署网站时就会在网页HTML文件中自动嵌入OG标签。\n谷歌搜索优化 在将我们的站点信息提交给谷歌时，谷歌需要验证我们对网站的所有权。验证方式有好几种，例如\n在网站根目录下放一个谷歌生成的验证文件 在网页HTML文件头部嵌入谷歌生成的特定标签 使用谷歌分析的Tracking ID（或者Measurement ID） 由于stack主题集成了对谷歌分析的支持，这里我们使用第三种验证方式。\n开启谷歌分析 谷歌分析（Google Analytics）是一个分析网站流量的工具，用它可以统计网站的访问量等信息。\n首先前往谷歌分析官网注册谷歌分析的账号，也可以直接用已有的谷歌账号登录。现在的谷歌分析一般是谷歌分析4（Google Analytics 4），使用Measurement ID而非之前的Tracking ID来跟踪网站。\n参考：谷歌分析（Google Analytics）最新使用教程\n获取Measurement ID。具体可参见谷歌分析的帮助文档。下面是具体操作：\n在用户首页找到“Admin“选项，新建一个“Property”，按照说明填入必要的信息。 然后点击“Property”这一列中的“Data Streams”选项。 点击“Add stream”，选择“Web”，填入你的网站域名和网站名字。 再在“Property”页面点击刚添加的stream，就能看到一个以“G-”开头的Measurement ID。记录下你的网站的Measurement ID。 在网站根目录下的config/_default/config.yaml配置文件中找到“googleAnalytics\u0026quot;配置项，填入你的Measurement ID。\n1 2 # GA Tracking ID googleAnalytics: G-measuremntID 提交站点地图 站点地图（Site Map）是一个存储有站点网页信息的XML数据文件，通常命名为sitemap.xml，将它提交给搜索引擎，搜索引擎将可以获取我们网站的网页信息。\nHugo会在生成和部署网站时在public文件夹下自动生成sitemap.xml文件。\n我们把站点地图提交到谷歌搜索，具体说明可参见谷歌站长页面的说明，下面是具体操作：\n登录谷歌搜索控制台（Google Search Console）https://search.google.com/search-console，可以使用在谷歌分析注册的账号。\n点击左上角的“Add property”，选择右侧的“URL prefix”方式，输入以https开头的网站网址。在验证所有权的选项中选择“Google Analytics”，点击验证。如果你在上一步开启谷歌分析后使用Hugo重新部署了网站的话，就可以直接验证通过。\n提交站点地图文件sitemap.xml。在左侧菜单栏点击“Sitemaps”选项，然后在添加站点地图的页面填入sitemap.xml所在的URL。例如对于本站，由于是双语站点，Hugo在部署网站时会生成3个sitemap.xml文件，分别是/public/sitemap.xml、/public/zh-cn/sitemap.xml以及/public/en/sitemap.xml。\n注意，添加sitemap时不要漏了路径开头的斜杠/，即使网站域名后面已经有一个斜杠了，也不能省略。\n提交成功之后“status”会显示“success”。\nHugo生成的3个站点地图中，/public/sitemap.xml中的内容其实是指向/public/zh-cn/sitemap.xml和/public/en/sitemap.xml的，所以我们只提交一个/public/sitemap.xml就可以。\n一般在站点地图成功提交之后大约1到2天后，就可以看到自己的网站已经被谷歌收录了。可以在谷歌搜索框中输入site:xxx.com来查看某个网站是否被谷歌搜索收录。\n百度搜索优化 针对百度搜索的优化是在百度资源搜索平台上完成的。\n前往百度资源搜索平台，登录百度账号。\n点击“链接提交”，然后点击\u0026quot;添加站点\u0026quot;。输入你的网站域名，同样需要验证站点的所有权，这里选择下载验证文件，然后把验证文件放在网站static文件夹内，在上传到Github。最后点击“验证”即可。\n然后点击左侧菜单栏“资源提交”中的“普通收录”，在资源提交的页面下选择“sitemap”，输入sitemap.xml所在的URL就可以了。\n不过在百度提交sitemap有两个限制：\n不允许提交索引型sitemap 对新账号每天只允许提交一个sitemap文件 其他平台收录 其他平台收录就不仔细讲解了，提供一下链接供大家参考。都差不多一样的验证方式。\n搜狗收录\n搜狗收录不支持站点地图提交，需要你列出所有的url批量提交，每次提交20条，所以没有其他平台那么智能。每次新加新的网页还需要自己主动提交。\n搜狗搜索资源平台\nBing收录\nMicrosoft Bing Webmaster Tools\n360提交入口： https://info.so.com/site_submit.html\nBackdata 搜索引擎网址提交入口： https://backdata.net/submit-site.html\n参考：\n个人网站的建立过程（四）：网站的搜索引擎优化（SEO） 从零开始搭建个人博客网站系列 五、让搜索引擎收录你的个人博客网站 额外的一些知识 gh-pages 是GitHub 所提供的一个服务，简单来讲就是可以让你不用花钱也可以部署一个静态网页作为展示用，因此对于前端工程师来讲就非常方便而且很实用，但是部署方式有很多。\ngh-pages是github-pages的缩写，可以用于个人博客和项目介绍的网站服务。\ngh-pages也是github特殊的分支，用来存放网站相关的一些资源，通常网站地址为username.github.io/仓库名\n项目名与username.github.io一样的话，可以省略仓库名，跟github个人资料页面一样，所以这个仓库是一个特殊的仓库，默认会自动开启gh-pages服务。别的需要手动。\n虽然gh-pages 是属于免费的服务，基本上只要你持有GitHub 帐号就可以使用，但是它基本上有几个重点可以稍微注意一下：\n只能放置纯静态网页，也就是说没有后端的网页，例如PHP、Node.js、Python 等等，只能是纯HTML、CSS、JavaScript 等等，因为它并没有运算能力。 gh-pages 是以储存库为单位，也就是说每个储存库都可以有一个gh-pages 分支，但是每个储存库只能有一个gh-pages 分支，因此如果你想要部署多个网页，那么你就需要建立多个储存库。 gh-pages 的容量是有限制的，每个储存库的容量是1GB，如果你的网页超过这个容量，那么就无法部署。 gh-pages 的流量为每月100GB gh-pages 每小时只能部署10 次，如果是使用自己写的GitHub Actions 就没有这个限制（毕竟要花钱）。 免费的ssh 凭证 预设的网域是https://\u0026lt;username\u0026gt;.github.io/\u0026lt;repository\u0026gt;，如果你想要使用自己的网域，那么你就需要花钱购买网域，并且设定DNS 最后要稍微注意一下gh-pages 虽然是免费提供的静态网页托管服务，但是它并不能拿来作为商业用途或是违法用途，否则你的帐号可能会被封锁\n简单说一下如何查看一个仓库是否启用gh-pages：进入项目settings-pages查看即可，有绿色钩就启动成功，没有需要指定分支和根目录，保存，稍等一会就行。\n用gh-pages分支展示自己的项目 我们只需要将网页资源上传至gh-pages分支即可\n搭建项目网站：将项目网站资源推送到gh-pages分支上,静态资源必须提交了才会成功\n1 git subtree push --prefix=dist origin gh-pages # dist为项目网站的目录 拉取指定分支\n1 2 3 4 5 6 7 8 git fetch origin # 获取origin仓库的信息 git checkout -b aaa origin/aaa # 创建并检出分支 # git clone之后也也可以检出分支 git checkout gh-page # 失败用上面办法 # 在git clone 的时候可以指定分支-b选项 git clone -b url 如果要项目中不含自己网站的源码，可以忽略public目录，将public目录创建为一个私有仓库的子目录，然后将子目录作为，项目的gh-pages分支。\n通过GitHub Actions自动部署gh-pages 简单介绍一下GitHub Actions:\nGitHub Actions是一个自动化工具。 可以实现自动化构建、测试、和部署项目。 定义自动化过程是通过编写workflows（工作流）实现的，格式是yaml。 推送部署的github-pages需要git账户认证，方式是SSH秘钥认证。\n所以需要设置ssh秘钥。\n生成秘钥：在bash中运行ssh-keygen,秘钥类型默认为rsa。可以给这个秘钥设置备注加-C选项，参数一般为拥有者邮箱，一直回车就行。秘钥保存位置：默认用户主目录下的.ssh。公钥就是id_rsa.pub\n建议将生成的这个秘钥对与本机认证的ssh秘钥对分开，保存到另外的一个地方。回车的时候修改位置就行。 设置公钥：\ngithub账户Settings-\u0026gt;SSH and GPG keys-\u0026gt;New SSH key将公钥复制粘贴保存就行。这种方式，只有有私钥，就能操作所有仓库，不推荐使用。 (自动构建之后)选择要推送的仓库Settings-\u0026gt;Deploy keys-\u0026gt;Add deploy key将公钥复制粘贴保存就行。title随意。注意勾选Allow write access。只针对该仓库有权限。推荐使用。 设置私钥：\n进入Actions所在的仓库Settings-\u0026gt;Secrets and variables-\u0026gt;Actions-\u0026gt;New repository secret。秘钥名称为ACTIONS_DEPLOY_KEY，值为私钥id_rsa的文件内容。最后保存就行。 添加workflows配置文件\n在构建仓库的根目录下创建.github/workflows目录，然后创建auto-deploy-gh-pages.yaml文件，内容如下：\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 45 46 47 48 49 50 51 52 53 54 55 name: Deploy github pages on: push: branches: - main # main 更新触发 # Allows you to run this workflow manually from the Actions tab workflow_dispatch: jobs: auto-deploy-github-pages: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: submodules: true # clone submodules fetch-depth: 0 # 克隆所有历史信息 - name: Setup Hugo uses: peaceiris/actions-hugo@v3 with: hugo-version: \u0026#34;0.125.4\u0026#34; # Hugo 版本 extended: true # hugo插件版 Stack主题 必须启用 - name: Cache resources # 缓存 resource 文件加快生成速度 uses: actions/cache@v4 with: path: resources # 检查照片文件变化 key: ${{ runner.os }}-hugocache-${{ hashFiles(\u0026#39;content/**/*\u0026#39;) }} restore-keys: ${{ runner.os }}-hugocache- - name: Build # 生成网页 删除无用 resource 文件 削减空行 run: hugo --minify --gc - name: Deploy # 部署到 GitHub Page uses: peaceiris/actions-gh-pages@v3 with: # 如果在同一个仓库下使用请使用 github_token 并注释 deploy_key # github_token: ${{ secrets.GITHUB_TOKEN }} deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} # 如果在同一个仓库请注释 external_repository: arlettebrook/arlettebrook.github.io # 你的 GitHub page 仓库 example/example.github.io publish_branch: main # 默认gh-pages # cname: blog.trojan123.top # 自定义域名 publish_dir: ./public user_name: \u0026#34;github-actions[bot]\u0026#34; user_email: \u0026#34;github-actions[bot]@users.noreply.github.com\u0026#34; # full_commit_message: ${{ github.event.head_commit.message }} # 不带提交哈希 # full_commit_message: Deploy from ${{ github.repository }}@${{ github.sha }} 🚀 commit_message: ${{ github.event.head_commit.message }}🚀 # 带提交哈希 # full_commit_message: Deploy from ${{ github.repository }}@${{ github.sha }}🚀 ${{ github.event.head_commit.message }} 注意你要将external_repository项里的arlettebrook/arlettebrook.github.io改为你要推送的仓库。cname为你绑定的自定义域名。\n忽略不必要的文件\n在构建项目根目录下创建.gitignore文件，内容如下：\n1 2 3 4 public resources assets/jsconfig.json .hugo_build.lock 最后将构建项目推送到远程就行，这样每次推送构建项目的main分支到远程，就会自动构建并推送到指定仓库。\n查看是否构建成功：进入构建项目的Actions选项里面即可查看。\n参考：（2）部署你的Hugo博客\n","date":"2024-04-22T16:12:26+08:00","image":"https://arlettebrook.github.io/p/%E5%9F%BA%E4%BA%8Ehugo%E5%92%8Cgh-pages%E5%BF%AB%E9%80%9F%E6%90%AD%E5%BB%BA%E9%9D%99%E6%80%81%E7%BD%91%E7%AB%99/hugo_hu4699868770670889127.jpg","permalink":"https://arlettebrook.github.io/p/%E5%9F%BA%E4%BA%8Ehugo%E5%92%8Cgh-pages%E5%BF%AB%E9%80%9F%E6%90%AD%E5%BB%BA%E9%9D%99%E6%80%81%E7%BD%91%E7%AB%99/","title":"基于Hugo和gh-pages快速搭建静态网站"}]