Fork me on GitHub

大大方方去做想要做的事情

TCP报文结构和功能简析

1、简介

     TCP:传输、控制、协议。

     TCP 与UDP 最大却别就在那个C上面,它充分实现了数据传输时各种控制功能。可以进行丢包重发控制,还可以对次序乱掉的数据包进行顺序控制,还能控制传输流量,这些是UDP中没有的。即 TCP 提供一种面向连接的、可靠的字节流服务。TCP 是一中面向有链接的协议,只有在确认对端存在的时候,才会发送分数据,从而也可以控制通信流量的浪费。

     什么是可靠的传输:不丢包、不损坏、不乱序、不重复。TCP 通过校验和、序列号、确认应答、重发控制、连接管理以及窗口控制等机制来实现可靠传输。接收端查询就收数据 TCP 首部中的序号和数据长度。将自己下一步应该接受的序列号作为确认应答返送回去。就这样,通过序列号和确认应答,TCP 实现可靠传输。一般使用 TCP 首部用于控制的字段来管理连接。一个连接的建立和断开,正常过程中,至少需要来回共 7 个包才能完成。

2、TCP首部

     TCP首部的数据结构如图所示:

TCP首部

     为了便于理解,忽略选项部分,固定首部通常为20个字节,将按作用分类分析。

2.1、端口号(port)

     前 4 个字节来标识了发送方的端口号和接收方的端口号,即该数据包由谁发送,由谁接收。前 2 个字节标识源端口号,紧接着 2 个字节标识目的端口号。

     即发送方:(11111111,1111111)2 = (65535)10,除去0~1023。

     即接收方:(11111111,1111111)2 = (65535)10,除去0~1023。

2.2、序号(seq)

     TCP 是面向字节流的。在一个 TCP 连接中传送的字节流中的每一个字节都按顺序编号。整个要传送的字节流的起始序号必须在连接建立时设置。首部中的序号字段值则是指的是本报文段所发送的数据的第一个字节的序号。长度为 4 字节,序号是 32bit 的无符号数,序号到达 2(32次方) - 1 后又从 0 开始。

2.3、确认号(ack)

     ack:确认序号,即确认字节的序号,更确切地说,是发送确认的一端所期望收到的下一个序号。所谓的发送确认的一端就是将确认信息发出的一端。比如第二次握手的S端就是发送确认的一端。确认序号为上次接收的最后一个字节序号加1.只有确认标志位(ACK)为1的时候,确认序号才有效。

2.4、数据偏移

     也叫首部长度,占4个bit,它指出TCP报文段的数据起始处距离TCP报文段的起始处有多远。

TCP首部

     由于首部中还有长度不确定的选项字段,因此数据偏移字段是必要的。“首部长度”是 4 位二进制数,单位是32位字,能表示的最大十进制数字是 15。(1111)2=(15)10,即是 15 个 32 位,一个 32 位是 4 个字节,因此数据偏移的最大值是 15x4=60 个字节,这也是 TCP 首部的最大字节。因为固定首部的存在,数据偏移的值最小为20个字节,因此选项长度不能超过40字节*(减去20个字节的固定首部)。

2.5、保留(reserve)

     占 6 位,保留为今后使用,但目前应置为 0。

2.6、紧急URG(urgent)

     当 URG=1 时,表明紧急指针字段有效。

     它告诉系统此报文段中有紧急数据,应尽快发送(相当于高优先级的数据),而不要按原来的排队顺序来传送。

     例如,已经发送了很长的一个程序要在远地的主机上运行。但后来发现了一些问题,需要取消该程序的运行,因此用户从键盘发出中断命令。如果不使用紧急数据,那么这两个字符将存储在接收TCP的缓存末尾。只有在所有的数据被处理完毕后这两个字符才被交付接收方的应用进程。这样做就浪费了很多时间。

     当 URG 置为 1 时,应用进程就告诉 TCP 有紧急数据要传送。于是 TCP 就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍然是普通数据。这时要与首部中紧急指针(Urgent Pointer)字段配合使用。

2.7、确认ACK(acknowledgment)

     仅当ACK = 1时确认号字段才有效,当ACK = 0时确认号无效。TCP规定,在连接建立后所有的传送的报文段都必须把ACK置为1。

2.8、推送 PSH(push)

     当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应。在这种情况下,TCP就可以使用推送(push)操作。发送方TCP把PSH置为1,并立即创建一个报文段发送出去。接收方TCP收到PSH=1的报文段,就尽快地(即“推送”向前)交付接收应用进程。而不用再等到整个缓存都填满了后再向上交付。

2.9、复位RST(reset)

     当RST=1时,表明TCP连接中出现了严重错误(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立传输连接。RST置为1还用来拒绝一个非法的报文段或拒绝打开一个连接。

2.10、同步SYN(synchronization)

     在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1和ACK=1。

     因此SYN=1就表示这是一个连接请求或连接接受报文。

2.11、终止FIN(finis,意思是“完”“终”)

     用来释放一个连接。当FIN=1时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。

2.12、窗口

     占2字节。窗口值是(0,216 -1)之间的整数。窗口指的是发送本报文段的一方的接受窗口(而不是自己的发送窗口),窗口大小是给对方用的。窗口值告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方一次发送的数据量(以字节为单位)。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。

     总之,窗口值作为接收方让发送方设置其发送窗口的依据。

     例如,A发送了一个报文段,其确认号是3000,窗口字段是1000.这就是告诉对方B:“从3000算起,A接收缓存空间还可接受1000个字节数据,字节序号是3000-3999”,可以想象到河道的阀门。

     总之:窗口字段明确指出了现在允许对方发送的数据量。窗口值经常在动态变化。

2.13、检验和

     占2字节。检验和字段检验的范围包括首部和数据这两部分。和UDP用户数据报一样,在计算检验和时,要在TCP报文段的前面加上12字节的伪首部。伪首部的格式和UDP用户数据报的伪首部一样。但应把伪首部第4个字段中的17改为6(TCP的协议号是6);把第5字段中的UDP中的长度改为TCP长度。接收方收到此报文段后,仍要加上这个伪首部来计算检验和。若使用TPv6,则相应的伪首部也要改变。

2.14、紧急指针

     占2字节。紧急指针仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据) 。因此,在紧急指针指出了紧急数据的末尾在报文段中的位置。当所有紧急数据都处理完时,TCP就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为0时也可以发送紧急数据。

2.15、选项

     长度可变,最长可达40个字节。当没有使用“选项”时,TCP的首部长度是20字节。

2.16、最大报文段长度

     最大报文段长度(MSS:Maximum Segment Size)表示TCP传往另一端的最大块数据的长度。当一个连接建立时,连接的双方都要通告各自的MSS。

     当建立一个连接时,每一方都有用于通告它期望接收的MSS选项(MSS选项只能出现在SYN报文段中),如果一方不接收来自另一方的MSS值,则MSS就定为默认值536字节(这个默认值允许20字节的IP首部和20字节的TCP首部以适合576字节IP数据报) 。

     为什么要规定一个最大报文长度MSS呢?这并不是考虑接受方的接收缓存可能存放不下TCP报文段中的数据。实际上,MSS与接收窗口值没有关系。我们知道,TCP报文段的数据部分,至少要加上40字节的首部(TCP首部20字节和IP首部20字节,这里还没有考虑首部中的可选部分)才能组装成一个IP数据报。若选择较小的MSS长度,网络的利用率就降低。设想在极端情况下,当TCP报文段只含有1字节的数据时,在IP层传输的数据报的开销至少有40字节(包括TCP报文段的首部和IP数据报的首部)。这样,对网络的利用率就不会超过1/41。到了数据链路层还要加上一些开销。但反过来,若TCP报文段非常长,那么在IP层传输时就有可能要分解成多个短数据报片。在终点要把收到的各个短数据报片组成成原来的TCP报文段,当传输出错时还要进行重传,这些也都会使开销增大。
     因此,MSS应尽可能大些,只要在IP层传输时不需要分片就行。

     由于IP数据报所经历的路径是动态变化的,因此在这条路径上确定的不需要的分片的MSS,如果改走另一条路径就可能需要进行分片。因此最佳的MSS是很难确定的。在连接过程中,双方都把自己能够支持的MSS写入这一字段,以后就按照这个数值传输数据,两个传送方向可以有不同的MSS值。若主机未填写这一项,则MSS的默认值是536字节长。因此,所有在互联网上的主机都应该接受的报文段长度是536+20(固定首部长度)=556字节。

     后来又增加了几个选项如窗口扩大选项、时间戳选项等。

2.17、窗口扩大选项

     窗口扩大选项是为了扩大窗口。

     我们知道,TCP首部中窗口字段长度是16位,因此最大的窗口大小为64K字节。虽然这对早期的网络是足够用的,但对于包含卫星信道的网络,传播时延和宽带都很大,要获得高吞吐量需要更大的窗口大小。

     窗口扩大选项占3字节,其中有一个字节表示移位值S。新的窗口值等于TCP首部中的窗口位数从16增大到(16+S)。移位值允许使用的最大值是14,相当于窗口最大值增大到2(16+14)-1=230-1。

     窗口扩大选项可以在双方初始建立TCP连接时进行协商。如果连接的某一端实现了窗口扩大,当它不再需要扩大其窗口时,可发送S=0选项,使窗口大小回到16。

2.18、时间戳选项

     时间戳选项占10字节,其中最主要的字段是时间戳字段(4字节)和时间戳回送回答字段(4字节)。时间戳选项有以下两个概念:

     第一、 用来计算往返时间RTT。发送方在发送报文段时把当前时钟的时间值放入时间戳字段,接收方在确认该报文段时把时间戳字段复制到时间戳回送回答字段。因此,发送方在收到确认报文后,可以准确地计算出RTT来。

     第二、 用于处理TCP序号超过232 的情况,这又称为防止序号绕回PAWS。我们知道,TCP报文段的序号只有32位,而每增加232 个序号就会重复使用原来用过的序号。当使用高速网络时,在一次TCP连接的数据传送中序号很可能被重复使用。例如,当使用1.5Mbit/s的速度发送报文段时,序号重复要6小时以上。但若用2.5Gbit/s的速率发送报文段,则不到14秒钟序号就会重复。为了使接收方能够把新的报文段和迟到很久的报文段区分开,则可以在报文段中加上这种时间戳。

数据链路层

1、概述

     数据链路层是TCP/IP协议栈的第二层!数据链路层的传输单元:帧(也就是传输单位)。

数据链路层

     帧的结构如下:

  • 帧结构的构成:MAC子层 + 上三层数据 + FCS

帧结构

  • 比喻:一个帧我们可以理解为一辆火车,MAC子层是火车头,上三层数据为乘客,FCS为火车尾巴
  • MAC子层头部包含(也叫帧头):目标MAC地址(6字节) 源MAC地址(6字节) 类型(2字节)
  • MAC地址:也称为物理地址,是被固化到网卡的全球唯一标识,如下图:

Mac 地址结构

     注释:MAC地址=厂家标识+内部编号====实现了全球唯一!怎么查看自己的MAC地址?开始运行–cmd–ipconfig /all

  • 类型字段的作用:区分上层协议,0806代表上层协议是ARP协议,0800代表上层是IP协议
  • 上三层数据:也就是3层包头+4层包头+5层数据。其中一个帧是有最大承载能力限制的。也就是一个帧中的上三层数据就是乘客,而一辆火车中的乘客是又上限的,一个帧的最大承受能力叫MTU值,目前国际标准为1500字节
  • MTU:(最大传输单元)1500字节
  • 帧尾:FCS=帧校验,长度4个字节,作用是校验整个帧在传输过程中是否发生传输错误。

     帧结构最终效果图如下:

帧结构效果图

     经典问题:请描述一下帧结构?

     答:帧是由帧头+上三层数据+帧尾,帧头包含目MAC,源MAC,类型,帧尾是FCS,MTU:1500

2、本层设备

     工作在2层的设备:交换机/网桥

3、交换机的工作原理

     经典问题:请描述一下交换机的工作原理。

     答:

     1)当收到一个帧,首先学习帧中的MAC地址来形成自己的MAC地址表!

     2)然后检查帧中的目标MAC地址,并匹配MAC地址表。

        如表中匹配成功,则单播转发!

        如表中无匹配项,则广播转发!

     3)MAC地址表的老化时间是?300秒!

     效果图如下:

交换机的工作原理

4、如何配置交换机

     傻瓜式交换机一般是不支持管理和配置的!企业级交换机支持配置高级功能及高级配置,价格要高,一般称为管理型交换机!如购买一台华为或者思科交换机,看下图:

交换机

     一般会自带一根console线!看下图:

console线

     建议再买一根com口转USB线,看下图:

com口转USB线

     使用console线+转换usb线,来连接交换机的console口与电脑的USB接口,如下图:

交换机接线

     然后再电脑上打开超级终端(xp上自带,win7另行下载即可),即可看到配置界面。当然我们可以使用思科的模拟软件来做实验,如cisco packettracer

谈谈云原生

     之前关于云计算技术底座的部门会谈,我本着程序员实事求是的态度,表示自己其实并不懂云原生。前段时间云技术底座的模型验证,遇到一个测试案例叫做“白屏纳管”,意思是指 CAAS 平台能够对包括云下硬件负载 F5、A10,云上 SLB 等设备进行管理。云原生技术有着太多的这种花里胡哨的名词,将本来很简单的一件事情包装出个能吓住人的词,来提高理解的门槛。作为一个一线开发,我有一种很深切的感受:很多优秀的设计,都是有着简洁优雅的设计原理或者说思想蕴含其中。这种没什么太多内涵的毫无意义的造词运动,对于工程实践,没有任何好处。我们不应该人云亦云盲目从众,也不应该以偏概全一叶障目。

     以下是阿里云公众号某产品经理关于云原生的解释,我截了图如下:

阿里云云原生公众号某文章

     这段话讲得没有问题,但是似乎又什么都没讲,对于云原生的定义,这种似是而非的说法显然不是一线开发人员想要的答案。

     我们在说云原生的时候,其实是在说“云原生计算”,云原生这个词其实可以拆为“云”和”原生“两个词,这其中其实隐藏了一个词——“云计算”。云原生一定是云计算, 这就要求我们事先已经对云计算的发展历史有所了解。云原生与云计算的区别便在“原生”二字上,那么理解云原生,重点就在理解其为何为“原生”。我这里用一句话来总结云原生:云原生是为了发挥出云计算所有优势的最短路径(这句精辟的提炼摘自《阿里云云原生架构实践》一书)。

     这些想法和做法归纳为三个方面:应用架构、计算模型、代表技术。

  • 代表技术 是最容易理解的。代表技术可以是一些狭义上的云原生基础设施,包含了相关的软件或硬件技术,例如裸金属、docker、K8S;也可以是泛化的一些工程技术体系,比如 Spring Cloud,DevOps,DDD ——它们与云基础设施不一样,很多并非为了云原生而生的,但是在云上环境表现活跃,也可以归纳到云原生代表技术中来。有些人说,使用了容器技术,就是云原生,这肯定是不准确的。容器其实只是改变了我们应用的部署方式,应用运行时的形态,以此来定义云原生是非常片面的。

  • 计算模型, 既然这个行业这么喜欢遣词造句,那我也造几个词(切,谁还不会啊!!!)。计算模型可以从两个方面来看:

     计算的服务模型: 计算的服务模型是从商业的角度看的。我们从传统的购买硬件送软件,到购买软件和维护再到如今购买云服务,云计算平台将计算、存储、网络像煤电一样卖给我们。需要指出,计算资源的这种服务模型并不是在云原生中才强调的,而是从云计算提出的时候,就已经强调了的。

     服务的计算模型: 这个语境中,“服务”不再是商业上的“服务”,而是指“应用服务”。应用服务的计算模型,强调了应用程序的可伸缩性、弹性、自动化和可维护性,以适应现代云环境中的需求。当然,应用的可伸缩、弹性自动化运维这些并不是应用自身具备的能力,而是在云上环境被赋予的,但是需要从应用侧做一定的改造工作,“以适应现代云环境中的需求”,比如下面要说的——应用架构。

  • 应用架构 为了最大化发挥云原生的计算优势,应用侧应该也要做架构升级——例如微服务化,在弹性扩缩的时候以更细的粒度进行算力分配,更精确的分配云计算底座资源(计算、存储、网络)——当然,这只是我简单作示意的一种说法。这种说法换个侧面来看,单体应用就不能上云计算么?当然可以,但是那就不叫云原生了,因为单体无法发挥出云计算的最大优势。

     所以从我自己目前的工作经历总结如下: 云原生一定是云计算,特别的,云原生是有效发挥出云计算所有优势的最短路径。理解云原生,可以从代表技术、云原生计算模型、云原生应用架构三方面着手理解。

设计模式之模板方法模式和策略模式

     这篇博客的设计模式应用案例来自于这个仓库,完整代码可以参考本仓库:

     微信大模型接入

仓库主页

     简单介绍一下这个仓库:1)实现了微信接入;2)实现了大模型接入;3)将微信的提问发给大模型,将大模型的回答返回给微信(欢迎给个 star)。

1、背景分析

     1)我们预期接入的大模型肯定不止一种,现在市面上除了最牛的 GhatGPT,国内也陆续退出了豆包、文心一言、星火大模型等。为了获得良好的扩展性,我们可以基于策略模式对模型通讯模块进行封装,将不同的模型定义为一种通讯策略,程序中可以通过参数指定不同的模型工作;

     2)通讯的过程无非就是三个阶段:通信前参数组装、进行通讯、通讯完成处理结果,这里显然是可以通过模板方法进行封装的。结合策略模式,我们可以规定将来接入新模型的时候,有统一的代码组织形式和良好的扩展接口。

2、知识补充

     我们这里不再对设计模式本身进行专门的讲解。

     策略模式

     模板方法模式

3、代码分析

     如下图所示,DefaultHandler 是程序写给微信接入模块的一个回调(实现了消息处理接口 IMsgHandlerFace),当微信接入模块接收到微信消息,便会触发此回调,执行用户预定义行为。也就是在这个地方,我们接入了大模型,并将模型的问答结果返回给微信。

模型接入的地方

     一下三行代码的作用分别是:获取聊天模型的策略上下文(请参考上文中菜鸟教程——策略模式),返回的策略上下文会包含具体的执行策略,执行策略的选择是程序参数定义的。

1
2
3
4
5
6
// 聊天模型策略
StrategyContext context = getStrategyContext();
// 构建聊天请求
ChatRequest request = buildChatRequest(msg);
// 进行聊天
ChatResponse response = context.executeStrategy(request);

程序参数指定执行策略

3、策略模式实现

     首先,定义策略接口,策略接口中的 exec 方法是所有具体的策略类都需要实现的。

策略接口

     定义策略上下文,上下文是统一交给用户侧的一个”句柄”(可以参考上文 DefaultHandler 的代码,用户侧通过获取策略上下文来执行具体的策略实现的),用于持有具体的策略实现。

策略上下文

     我们这里的策略实现类稍有不同,没有直接实现 exec 方法,也没有直接实现 IStrategy 接口。这涉及到另外一个设计模式——模板方法模式。

策略实现类

4、 模板方法模式

     抽象策略类定义了一个算法模板方法,这个模板方法规定了 exec 方法执行时发生的三个算法步骤:postChatRequest (执行前的参数处理)、doExec (执行通讯请求)、postChatResponse(通讯完成之后的响应报文处理)。但是我并没有对这三个步骤进行实现,他们都是抽象的,延迟到了将来的策略实现类去实现。

抽象策略类

     所有的策略实现类,实现的不是策略接口 IStrategy,而是继承抽象策略类 AbstractStrategy,也不再去实现 exec 方法,而是实现抽象类中算法模板规定的三个算法步骤。

实现抽象策略类中规定的三个既定步骤

5、 其他一些想法

     面向对象语言最重要的三个基本特性:封装、多态、继承,是软件工程七大原则开闭原则,里氏代换原则,依赖倒转原则,接口隔离原则,迪米特原则和合成复用原则的重要支撑点,设计模式是一种如何最大化发挥三个基本特性,从而能够遵循七大原则的一种编码层级上的技术,这也是 Java、C++ 等完美支持 OOP 编程范式语言,在面临庞大复杂工程时,总能将源代码组织得很好的原因之一吧。从这个角度出发,Go 语言在多态、继承的表现力上不足,也许是因为我还比较缺乏 Go 开发的实战经验,所以我不确定在面临复杂的建模场景的时候,Go 语言的编程方式还能不能进行有效表达。

Go基础语法宝典

关键字

Go语言设计的关键字,了解这些关键字有助于命名变量的冲突避免

go的二十五个关键字

1
2
3
4
5
break    default      func    interface    select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var

简介

  • varconst 是 Go语言基础里面的变量和常量申明

  • packageimport 用于分包和导入

  • func 用于定义函数和方法

  • return 用于从函数返回

  • defer 用于类似析构函数

  • go 用于并发

  • select 用于选择不同类型的通讯

  • interface 用于定义接口

  • struct 用于定义抽象数据类型

  • breakcasecontinueforfallthroughelseifswitchgotodefault 用于流程控制

  • chan用于channel通讯

  • type用于声明自定义类型

  • map用于声明map类型数据

  • range用于读取slice、map、channel数据

数据类型的定义

定义变量

Go语言里面定义变量有多种方式。

使用var关键字是Go最基本的定义变量方式,与C语言不同的是Go把变量类型放在变量名后面:

1
2
//定义一个名称为“variableName”,类型为"type"的变量
var variableName type

定义多个变量

1
2
//定义三个类型都是“type”的变量
var vname1, vname2, vname3 type

定义变量并初始化值

1
2
//初始化“variableName”的变量为“value”值,类型是“type”
var variableName type = value

同时初始化多个变量

1
2
3
4
5
/*
定义三个类型都是"type"的变量,并且分别初始化为相应的值
vname1为v1,vname2为v2,vname3为v3
*/
var vname1, vname2, vname3 type= v1, v2, v3

是不是觉得上面这样的定义有点繁琐?有一种写法可以让它变得简单一点。可以直接忽略类型声明,那么上面的代码变成这样了:

1
2
3
4
5
6
/*
定义三个变量,它们分别初始化为相应的值
vname1为v1,vname2为v2,vname3为v3
然后Go会根据其相应值的类型来初始化它们
*/
var vname1, vname2, vname3 = v1, v2, v3

觉得上面的还是有些繁琐,继续简化:

1
2
3
4
5
6
/*
定义三个变量,它们分别初始化为相应的值
vname1为v1,vname2为v2,vname3为v3
编译器会根据初始化的值自动推导出相应的类型
*/
vname1, vname2, vname3 := v1, v2, v3

现在是不是看上去非常简洁了?:=这个符号直接取代了vartype,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用var方式来定义全局变量。

_(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,将值35赋予b,并同时丢弃34

1
_, b := 34, 35

Go对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了i但未使用。

1
2
3
4
package main
func main() {
var i int
}

常量

所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。

它的语法如下:

1
2
3
const constantName = value
//如果需要,也可以明确指定常量的类型:
const Pi float32 = 3.1415926

下面是一些常量声明的例子:

1
2
3
4
const Pi = 3.1415926
const i = 10000
const MaxThread = 10
const prefix = "astaxie_"

Go 常量和一般程序语言不同的是,可以指定相当多的小数位数(例如200位),若指定给float32自动缩短为32bit,指定给float64自动缩短为64bit,详情参考 http://golang.org/ref/spec#Constants (需科学上网)

内置基础类型

Boolean

在Go中,布尔值的类型为bool,值是truefalse,默认为false

1
2
3
4
5
6
7
8
//示例代码
var isActive bool // 全局变量声明
var enabled, disabled = true, false // 忽略类型的声明
func test() {
var available bool // 一般声明
valid := false // 简短声明
available = true // 赋值操作
}

数值类型

整数类型有无符号和带符号两种。Go同时支持intuint,这两种类型的长度相同,但具体长度取决于不同编译器的实现。Go里面也有直接定义好位数的类型:rune, int8, int16, int32, int64byte, uint8, uint16, uint32, uint64。其中runeint32的别称,byteuint8的别称。

需要注意的一点是,这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。

如下的代码会产生错误:invalid operation: a + b (mismatched types int8 and int32)

var a int8

var b int32

c:=a + b

另外,尽管int的长度是32 bit, 但int 与 int32并不可以互用。

浮点数的类型有float32float64两种(没有float类型),默认是float64

Go还支持复数。它的默认类型是complex128(64位实数+64位虚数)。如果需要小一些的,也有complex64(32位实数+32位虚数)。复数的形式为RE + IMi,其中RE是实数部分,IM是虚数部分,而最后的i是虚数单位。下面是一个使用复数的例子:

1
2
3
var c complex64 = 5+5i
//output: (5+5i)
fmt.Printf("Value is: %v", c)

字符串

Go中的字符串都是采用UTF-8字符集编码。字符串是用一对双引号("")或反引号( )括起来定义,它的类型是string

1
2
3
4
5
6
7
8
//示例代码
var frenchHello string // 声明变量为字符串的一般方法
var emptyString string = "" // 声明了一个字符串变量,初始化为空字符串
func test() {
no, yes, maybe := "no", "yes", "maybe" // 简短声明,同时声明多个变量
japaneseHello := "Konichiwa" // 同上
frenchHello = "Bonjour" // 常规赋值
}

在Go中字符串是不可变的,例如下面的代码编译时会报错:cannot assign to s[0]

1
2
var s string = "hello"
s[0] = 'c'

但如果真的想要修改怎么办呢?下面的代码可以实现:

1
2
3
4
5
s := "hello"
c := []byte(s) // 将字符串 s 转换为 []byte 类型
c[0] = 'c'
s2 := string(c) // 再转换回 string 类型
fmt.Printf("%s\n", s2)

Go中可以使用+操作符来连接两个字符串:

1
2
3
4
s := "hello,"
m := " world"
a := s + m
fmt.Printf("%s\n", a)

修改字符串也可写为:

1
2
3
s := "hello"
s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作
fmt.Printf("%s\n", s)

如果要声明一个多行的字符串怎么办?可以通过```来声明:

1
2
m := `hello
world`

``` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。例如本例中会输出:

1
2
hello
world

错误类型

Go内置有一个error类型,专门用来处理错误信息,Go的package里面还专门有一个包errors来处理错误:

1
2
3
4
err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
fmt.Print(err)
}

分组声明

在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。

例如下面的代码:

1
2
3
4
5
6
7
8
import "fmt"
import "os"
const i = 100
const pi = 3.1415
const prefix = "Go_"
var i int
var pi float32
var prefix string

可以分组写成如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import(
"fmt"
"os"
)
const(
i = 100
pi = 3.1415
prefix = "Go_"
)
var(
i int
pi float32
prefix string
)

iota枚举

Go里面有一个关键字iota,这个关键字用来声明enum的时候采用,它默认开始值是0,const中每增加一行加1:

1
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 (
"fmt"
)
const (
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
w // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota"
)
const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0
const (
h, i, j = iota, iota, iota //h=0,i=0,j=0 iota在同一行值相同
)
const (
a = iota //a=0
b = "B"
c = iota //c=2
d, e, f = iota, iota, iota //d=3,e=3,f=3
g = iota //g = 4
)
func main() {
fmt.Println(a, b, c, d, e, f, g, h, i, j, x, y, z, w, v)
}

除非被显式设置为其它值或iota,每个const分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是iota,则它也被设置为iota

Go程序设计的一些规则

Go之所以会那么简洁,是因为它有一些默认的行为:

  • 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公有变量;小写字母开头的就是不可导出的,是私有变量。
  • 大写字母开头的函数也是一样,相当于class中的带public关键词的公有函数;小写字母开头的就是有private关键词的私有函数。

arrayslicemap

array

array就是数组,它的定义方式如下:

1
var arr [n]type

[n]type中,n表示数组的长度,type表示存储元素的类型。对数组的操作和其它语言类似,都是通过[]来进行读取或赋值:

1
2
3
4
5
var arr [10]int  // 声明了一个int类型的数组
arr[0] = 42 // 数组下标是从0开始的
arr[1] = 13 // 赋值操作
fmt.Printf("The first element is %d\n", arr[0]) // 获取数据,返回42
fmt.Printf("The last element is %d\n", arr[9]) //返回未赋值的最后一个元素,默认返回0

由于长度也是数组类型的一部分,因此[3]int[4]int是不同的类型,数组也就不能改变长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。如果要使用指针,那么就需要用到后面介绍的slice类型了。

数组可以使用另一种:=来声明

1
2
3
a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组
b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0
c := [...]int{4, 5, 6} // 可以省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度

Go支持嵌套数组,即多维数组。比如下面的代码就声明了一个二维数组:

1
2
3
4
// 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}
// 上面的声明可以简化,直接忽略内部的类型
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}

slice

在很多应用场景中,数组并不能满足需求。在初始定义数组时,并不知道需要多大的数组,因此就需要“动态数组”。在Go里面这种数据结构叫slice

slice并不是真正意义上的动态数组,而是一个引用类型。slice总是指向一个底层arrayslice的声明也可以像array一样,只是不需要长度。

1
2
// 和声明array一样,只是少了长度
var fslice []int

接下来可以声明一个slice,并初始化数据,如下所示:

1
slice := []byte {'a', 'b', 'c', 'd'}

slice可以从一个数组或一个已经存在的slice中再次声明。slice通过array[i:j]来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i

1
2
3
4
5
6
7
8
9
10
// 声明一个含有10个元素元素类型为byte的数组
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 声明两个含有byte的slice
var a, b []byte
// a指向数组的第3个元素开始,并到第五个元素结束,
a = ar[2:5]
//现在a含有的元素: ar[2]、ar[3]和ar[4]
// b是数组ar的另一个slice
b = ar[3:5]
// b的元素是:ar[3]和ar[4]

注意slice和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用...自动计算长度,而声明slice时,方括号内没有任何字符。

slice有一些简便的操作

  • slice的默认开始位置是0,ar[:n]等价于ar[0:n]

  • slice的第二个序列默认是数组的长度,ar[n:]等价于ar[n:len(ar)]

  • 如果从一个数组里面直接获取slice,可以这样ar[:],因为默认第一个序列是0,第二个是数组的长度,即等价于ar[0:len(ar)]

下面这个例子展示了更多关于slice的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 声明一个数组
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 声明两个slice
var aSlice, bSlice []byte
// 演示一些简便操作
aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c
aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j
aSlice = array[:] // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素
// 从slice中获取slice
aSlice = array[3:7] // aSlice包含元素: d,e,f,g,len=4,cap=7
bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f
bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f
bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h
bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g

slice是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的aSlicebSlice,如果修改了aSlice中元素的值,那么bSlice相对应的值也会改变。

从概念上面来说slice像一个结构体,这个结构体包含了三个元素:

  • 一个指针,指向数组中slice指定的开始位置

  • 长度,即slice的长度

  • 最大长度,也就是slice开始位置到数组的最后位置的长度

1
2
Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
Slice_a := Array_a[2:5]

slice有几个有用的内置函数

  • len 获取slice的长度

  • cap 获取slice的最大容量

  • appendslice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice

  • copy 函数copy从源slicesrc中复制元素到目标dst,并且返回复制的元素的个数

注:append函数会改变slice所引用的数组的内容,从而影响到引用同一数组的其它slice

但当slice中没有剩余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的slice数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的slice则不受影响。

从Go1.2开始slice支持了三个参数的slice,之前一直采用这种方式在slice或者array基础上来获取一个slice

1
2
var array [10]int
slice := array[2:4]

这个例子里面slice的容量是8,新版本里面可以指定这个容量

1
slice = array[2:4:7]

上面这个的容量就是7-2,即5。这样这个产生的新的slice就没办法访问最后的三个元素。

如果slice是这样的形式array[:i:j],即第一个参数为空,默认值就是0。

map

1
map`也就是Python中字典的概念,它的格式为`map[keyType]valueType

看下面的代码,map的读取和设置也类似slice一样,通过key来操作,只是sliceindex只能是`int`类型,而map多了很多类型,可以是int,可以是string及所有完全定义了==!=操作的类型。

1
2
3
4
5
6
7
8
9
// 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化
var numbers map[string]int
// 另一种map的声明方式
numbers = make(map[string]int)
numbers["one"] = 1 //赋值
numbers["ten"] = 10 //赋值
numbers["three"] = 3
fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据
// 打印出来如:第三个数字是: 3

这个map就像平常看到的表格一样,左边列是key,右边列是值

使用map过程中需要注意的几点:

  • map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取

  • map的长度是不固定的,也就是和slice一样,也是一种引用类型

  • 内置的len函数同样适用于map,返回map拥有的key的数量

  • map的值可以很方便的修改,通过numbers["one"]=11可以很容易的把key为one的字典值改为11

  • map和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制

map的初始化可以通过key:val的方式初始化值,同时map内置有判断是否存在key的方式

通过delete删除map的元素:

1
2
3
4
5
6
7
8
9
10
// 初始化一个字典
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
// map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true
csharpRating, ok := rating["C#"]
if ok {
fmt.Println("C# is in the map and its rating is ", csharpRating)
} else {
fmt.Println("We have no rating associated with C# in the map")
}
delete(rating, "C") // 删除key为C的元素

上面说过了,map也是一种引用类型,如果两个map同时指向一个底层,那么一个改变,另一个也相应的改变:

1
2
3
4
m := make(map[string]string)
m["Hello"] = "Bonjour"
m1 := m
m1["Hello"] = "Salut" // 现在m["hello"]的值已经是Salut了

makenew操作

make用于内建类型(mapslicechannel)的内存分配。new用于各种类型的内存分配。

内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要:

new返回指针。

内建函数make(T, args)new(T)有着不同的功能,make只能创建slicemapchannel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slicenil。对于slicemapchannel来说,make初始化了内部的数据结构,填充适当的值。

make返回初始化后的(非零)值。

零值

关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。

此处罗列 部分类型 的 “零值”

1
2
3
4
5
6
7
8
9
10
11
int     0
int8 0
int32 0
int64 0
uint 0x0
rune 0 //rune的实际类型是 int32
byte 0x0 // byte的实际类型是 uint8
float32 0 //长度为 4 byte
float64 0 //长度为 8 byte
bool false
string ""

流程控制

Go中流程控制分三大类:条件判断,循环控制和无条件跳转。

if

if也许是各种编程语言中最常见的了,它的语法概括起来就是:如果满足条件就做某事,否则做另一件事。

Go里面if条件判断语句中不需要括号,如下代码所示

1
2
3
4
5
if x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}

Go的if还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下所示

1
2
3
4
5
6
7
8
// 计算获取值x,然后根据x返回的大小,判断是否大于10。
if x := computedValue(); x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}
//这个地方如果这样调用就编译出错了,因为x是条件里面的变量
fmt.Println(x)

多个条件的时候如下所示:

1
2
3
4
5
6
7
if integer == 3 {
fmt.Println("The integer is equal to 3")
} else if integer < 3 {
fmt.Println("The integer is less than 3")
} else {
fmt.Println("The integer is greater than 3")
}

goto

Go有goto语句——请明智地使用它。用goto跳转到必须在当前函数内定义的标签。例如假设这样一个循环:

1
2
3
4
5
6
7
func myFunc() {
i := 0
Here: //这行的第一个词,以冒号结束作为标签
println(i)
i++
goto Here //跳转到Here去
}

标签名是大小写敏感的。

for

Go里面最强大的一个控制逻辑就是for,它既可以用来循环读取数据,又可以当作while来控制逻辑,还能迭代操作。它的语法如下:

1
2
3
for expression1; expression2; expression3 {
//...
}

expression1expression2expression3都是表达式,其中expression1expression3是变量声明或者函数调用返回值之类的,expression2是用来条件判断,expression1在循环开始之前调用,expression3在每轮循环结束之时调用。

一个例子比上面讲那么多更有用,看看下面的例子吧:

1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
func main(){
sum := 0;
for index:=0; index < 10 ; index++ {
sum += index
}
fmt.Println("sum is equal to ", sum)
}
// 输出:sum is equal to 45

有些时候需要进行多个赋值操作,由于Go里面没有,操作符,那么可以使用平行赋值i, j = i+1, j-1

有些时候如果忽略expression1expression3

1
2
3
4
sum := 1
for ; sum < 1000; {
sum += sum
}

其中;也可以省略,那么就变成如下的代码了,这就是while的功能。

1
2
3
4
sum := 1
for sum < 1000 {
sum += sum
}

在循环里面有两个关键操作breakcontinue ,break操作是跳出当前循环,continue是跳过本次循环。当嵌套过深的时候,break可以配合标签使用,即跳转至标签所指定的位置,详细参考如下例子:

1
2
3
4
5
6
7
8
for index := 10; index>0; index-- {
if index == 5{
break // 或者continue
}
fmt.Println(index)
}
// break打印出来10、9、8、7、6
// continue打印出来10、9、8、7、6、4、3、2、1

breakcontinue还可以跟着标号,用来跳到多重循环中的外层循环

for配合range可以用于读取slicemap的数据:

1
2
3
4
for k,v:=range map {
fmt.Println("map's key:",k)
fmt.Println("map's val:",v)
}

由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用_来丢弃不需要的返回值

例如

1
2
3
for _, v := range map{
fmt.Println("map's val:", v)
}

switch

有些时候需要写很多的if-else来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候switch就能很好的解决这个问题。它的语法如下

1
2
3
4
5
6
7
8
9
10
switch sExpr {
case expr1:
some instructions
case expr2:
some other instructions
case expr3:
some other instructions
default:
other code
}

sExprexpr1expr2expr3的类型必须一致。Go的switch非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;而如果switch没有表达式,它会匹配true

1
2
3
4
5
6
7
8
9
10
11
i := 10
switch i {
case 1:
fmt.Println("i is equal to 1")
case 2, 3, 4:
fmt.Println("i is equal to 2, 3 or 4")
case 10:
fmt.Println("i is equal to 10")
default:
fmt.Println("All I know is that i is an integer")
}

在第5行中,把很多值聚合在了一个case里面,同时,Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
integer := 6
switch integer {
case 4:
fmt.Println("The integer was <= 4")
fallthrough
case 5:
fmt.Println("The integer was <= 5")
fallthrough
case 6:
fmt.Println("The integer was <= 6")
fallthrough
case 7:
fmt.Println("The integer was <= 7")
fallthrough
case 8:
fmt.Println("The integer was <= 8")
fallthrough
default:
fmt.Println("default case")
}

上面的程序将输出

1
2
3
4
The integer was <= 6
The integer was <= 7
The integer was <= 8
default case

函数

函数的定义

函数是Go里面的核心设计,它通过关键字func来声明,它的格式如下:

1
2
3
4
5
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
//这里是处理逻辑代码
//返回多个值
return value1, value2
}

上面的代码可以看出

  • 关键字func用来声明一个函数funcName

  • 函数可以有一个或者多个参数,每个参数后面带有类型,通过,分隔

  • 函数可以返回多个值

  • 上面返回值声明了两个变量output1output2,如果不想声明也可以,直接就两个类型

  • 如果只有一个返回值且不声明返回值变量,那么可以省略 包括返回值的括号

  • 如果没有返回值,那么就直接省略最后的返回信息

  • 如果有返回值, 那么必须在函数的外层添加return语句

下面来看一个实际应用函数的例子(用来计算Max值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"
// 返回a、b中最大值.
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
x := 3
y := 4
z := 5
max_xy := max(x, y) //调用函数max(x, y)
max_xz := max(x, z) //调用函数max(x, z)
fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在这直接调用它
}

上面这个里面可以看到max函数有两个参数,它们的类型都是int,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型,同理多于2个同类型的变量或者返回值。同时注意到它的返回值就是一个类型,这个就是省略写法。

多个返回值

Go语言比C更先进的特性,其中一点就是函数能够返回多个值。

直接看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
//返回 A+B 和 A*B
func SumAndProduct(A, B int) (int, int) {
return A+B, A*B
}
func main() {
x := 3
y := 4
xPLUSy, xTIMESy := SumAndProduct(x, y)
fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}

上面的例子可以看到直接返回了两个参数,当然也可以命名返回参数的变量,这个例子里面只是用了两个类型,也可以改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。但如果函数是导出的(首字母大写),官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。

1
2
3
4
5
func SumAndProduct(A, B int) (add int, Multiplied int) {
add = A+B
Multiplied = A*B
return
}

变参

Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参:

1
func myfunc(arg ...int) {}

arg ...int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。在函数体中,变量arg是一个intslice

1
2
3
for _, n := range arg {
fmt.Printf("And the number is: %d\n", n)
}

传值与传指针

传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。

为了验证上面的说法,来看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
//简单的一个函数,实现了参数+1的操作
func add1(a int) int {
a = a+1 // 改变了a的值
return a //返回一个新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 应该输出 "x = 3"
x1 := add1(x) //调用add1(x)
fmt.Println("x+1 = ", x1) // 应该输出"x+1 = 4"
fmt.Println("x = ", x) // 应该输出"x = 3"
}

虽然调用了add1函数,并且在add1中执行a = a+1操作,但是上面例子中x变量的值没有发生变化

理由很简单:因为当调用add1的时候,add1接收的参数其实是x的copy,而不是x本身。

如果真的需要传这个x本身,该怎么办呢?

这就牵扯到了所谓的指针。变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有add1函数知道x变量所在的地址,才能修改x变量的值。所以需要将x所在地址&x传入函数,并将函数的参数的类型由int改为*int,即改为指针类型,才能在函数中修改x变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。请看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
//简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,
*a = *a+1 // 修改了a的值
return *a // 返回新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 应该输出 "x = 3"
x1 := add1(&x) // 调用 add1(&x) 传x的地址
fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
fmt.Println("x = ", x) // 应该输出 "x = 4"
}

这样,就达到了修改x的目的。那么到底传指针有什么好处呢?

  • 传指针使得多个函数能操作同一个对象。

  • 传指针比较轻量级 (8bytes),只是传内存地址,可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当要传递大的结构体的时候,用指针是一个明智的选择。

  • Go语言中channelslicemap这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)

defer

Go语言中有种不错的设计,即延迟(defer)语句,可以在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。特别是当进行一些打开资源的操作时,遇到错误需要提前返回,在返回前需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,一般写打开一个资源是这样操作的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func ReadWrite() bool {
file.Open("file")
// 做一些工作
if failureX {
file.Close()
return false
}
if failureY {
file.Close()
return false
}
file.Close()
return true
}

上面有很多重复的代码,Go的defer有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。在defer后指定的函数会在函数退出前调用。

1
2
3
4
5
6
7
8
9
10
11
func ReadWrite() bool {
file.Open("file")
defer file.Close()
if failureX {
return false
}
if failureY {
return false
}
return true
}

如果有很多调用defer,那么defer是采用后进先出模式,所以如下代码会输出4 3 2 1 0

1
2
3
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}

通常来说,defer会用在释放数据库连接,关闭文件等需要在函数结束时处理的操作。

函数作为值、类型

在Go中函数也是一种变量,可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型

1
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递,请看下面的例子

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
package main
import "fmt"
type testInt func(int) bool // 声明了一个函数类型
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
func isEven(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}
// 声明的函数类型在这个地方当做了一个参数
func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
}
return result
}
func main(){
slice := []int {1, 2, 3, 4, 5, 7}
fmt.Println("slice = ", slice)
odd := filter(slice, isOdd) // 函数当做值来传递了
fmt.Println("Odd elements of slice are: ", odd)
even := filter(slice, isEven) // 函数当做值来传递了
fmt.Println("Even elements of slice are: ", even)
}

函数当做值和类型在写一些通用接口的时候非常有用,通过上面例子看到testInt这个类型是一个函数类型,然后两个filter函数的参数和返回值与testInt类型是一样的,但是可以实现很多种的逻辑,这样使得程序变得非常的灵活。

Panic和Recover

Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panicrecover机制。一定要记住,应当把它作为最后的手段来使用,也就是说,代码中应当没有,或者很少有panic的东西。这是个强大的工具,请明智地使用它。

Panic

是一个内建函数,可以中断原有的控制流程,进入一个panic状态中。当函数F调用panic,函数F的执行被中断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panicgoroutine中所有调用的函数返回,此时程序退出。panic可以直接调用panic产生。也可以由运行时错误产生,例如访问越界的数组。

Recover

是一个内建的函数,可以让进入panic状态的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入panic状态,调用recover可以捕获到panic的输入值,并且恢复正常的执行。

下面这个函数演示了如何在过程中使用panic

1
2
3
4
5
6
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}

下面这个函数检查作为其参数的函数在执行时是否会产生panic

1
2
3
4
5
6
7
8
9
func throwsPanic(f func()) (b bool) {
defer func() {
if x := recover(); x != nil {
b = true
}
}()
f() //执行函数f,如果f中出现了panic,那么就可以恢复回来
return
}

注意:

defer必须在panic语句之前。

recover必须配合defer使用。

main函数和init函数

Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,强烈建议用户在一个package中每个文件只写一个init函数。

Go程序会自动调用init()main(),所以不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。

程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。

import

在写Go代码的时候经常用到import这个命令用来导入包文件,经常看到的方式参考如下:

1
2
3
import(
"fmt"
)

然后代码里面可以通过如下的方式调用

1
fmt.Println("hello world")

上面这个fmt是Go语言的标准库,其实是去GOROOT环境变量指定目录下去加载该模块,当然Go的import还支持如下两种方式来加载自己写的模块:

1、相对路径

1
import "./model" //当前文件同一目录的model目录,但是不建议这种方式来import

2、绝对路径

1
import "shorturl/model" //加载gopath/src/shorturl/model模块

上面展示了一些import常用的几种方式,但是还有一些

特殊的import

1、点操作

有时候会看到如下的方式导入包

1
2
3
import(
. "fmt"
)

这个点操作的含义就是这个包导入之后在调用这个包的函数时,可以省略前缀的包名,也就是前面调用的fmt.Println(“hello world”)可以省略的写成Println("hello world")

2、别名操作

别名操作顾名思义可以把包命名成另一个用起来容易记忆的名字

1
2
3
import(
f "fmt"
)

别名操作的话调用包函数时前缀变成了前缀,即f.Println("hello world")

3、_操作

这个操作经常是让很多人费解的一个操作符,请看下面这个import

1
2
3
4
import (
"database/sql"
_ "github.com/ziutek/mymysql/godrv"
)

_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数

struct类型

struct类型的声明

Go语言中,也和C或者其他语言一样,可以声明新的类型,作为其它类型的属性或字段的容器。例如,可以创建一个自定义类型person代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型称之struct。如下代码所示:

1
2
3
4
type person struct {
name string
age int
}

声明一个struct如此简单,上面的类型包含有两个字段

  • 一个string类型的字段name,用来保存用户名称这个属性
  • 一个int类型的字段age,用来保存用户年龄这个属性

使用struct看下面的代码

1
2
3
4
5
6
7
8
type person struct {
name string
age int
}
var P person // P现在就是person类型的变量了
P.name = "Astaxie" // 赋值"Astaxie"给P的name属性.
P.age = 25 // 赋值"25"给变量P的age属性
fmt.Printf("The person's name is %s", P.name) // 访问P的name属性.

除了上面这种P的声明使用之外,还有另外几种声明使用方式:

  1. 按照顺序提供初始化值
    P := person{"Tom", 25}

  2. 通过field:value的方式初始化,这样可以任意顺序
    P := person{age:24, name:"Tom"}

  3. 当然也可以通过new函数分配一个指针,此处P的类型为*person
    P := new(person)

看一个完整的使用struct的例子

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
package main
import "fmt"
// 声明一个新的类型
type person struct {
name string
age int
}
// 比较两个人的年龄,返回年龄大的那个人,并且返回年龄差
// struct也是传值的
func Older(p1, p2 person) (person, int) {
if p1.age>p2.age { // 比较p1和p2这两个人的年龄
return p1, p1.age-p2.age
}
return p2, p2.age-p1.age
}
func main() {
var tom person
// 赋值初始化
tom.name, tom.age = "Tom", 18
// 两个字段都写清楚的初始化
bob := person{age:25, name:"Bob"}
// 按照struct定义顺序初始化值
paul := person{"Paul", 43}
tb_Older, tb_diff := Older(tom, bob)
tp_Older, tp_diff := Older(tom, paul)
bp_Older, bp_diff := Older(bob, paul)
fmt.Printf("Of %s and %s, %s is older by %d years\n",
tom.name, bob.name, tb_Older.name, tb_diff)
fmt.Printf("Of %s and %s, %s is older by %d years\n",
tom.name, paul.name, tp_Older.name, tp_diff)
fmt.Printf("Of %s and %s, %s is older by %d years\n",
bob.name, paul.name, bp_Older.name, bp_diff)
}

struct的匿名字段

定义的时候是字段名与其类型一一对应,实际上Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。

当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct

看一个例子,让上面说的这些更具体化

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
package main
import "fmt"
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,那么默认Student就包含了Human的所有字段
speciality string
}
func main() {
// 初始化一个学生
mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
// 访问相应的字段
fmt.Println("His name is ", mark.name)
fmt.Println("His age is ", mark.age)
fmt.Println("His weight is ", mark.weight)
fmt.Println("His speciality is ", mark.speciality)
// 修改对应的备注信息
mark.speciality = "AI"
fmt.Println("Mark changed his speciality")
fmt.Println("His speciality is ", mark.speciality)
// 修改他的年龄信息
fmt.Println("Mark become old")
mark.age = 46
fmt.Println("His age is", mark.age)
// 修改他的体重信息
fmt.Println("Mark is not an athlet anymore")
mark.weight += 60
fmt.Println("His weight is", mark.weight)
}

看到Student访问属性age和name的时候,就像访问自己所有用的字段一样,匿名字段就是这样,能够实现字段的继承。student还能访问Human这个字段作为字段名。请看下面的代码。

1
2
mark.Human = Human{"Marcus", 55, 220}
mark.Human.age -= 1

通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段,所有的内置类型和自定义类型都是可以作为匿名字段的。请看下面的例子

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
package main
import "fmt"
type Skills []string
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,struct
Skills // 匿名字段,自定义的类型string slice
int // 内置类型作为匿名字段
speciality string
}
func main() {
// 初始化学生Jane
jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"}
// 现在访问相应的字段
fmt.Println("Her name is ", jane.name)
fmt.Println("Her age is ", jane.age)
fmt.Println("Her weight is ", jane.weight)
fmt.Println("Her speciality is ", jane.speciality)
// 修改他的skill技能字段
jane.Skills = []string{"anatomy"}
fmt.Println("Her skills are ", jane.Skills)
fmt.Println("She acquired two new ones ")
jane.Skills = append(jane.Skills, "physics", "golang")
fmt.Println("Her skills now are ", jane.Skills)
// 修改匿名内置类型字段
jane.int = 3
fmt.Println("Her preferred number is", jane.int)
}

从上面例子看出来struct不仅仅能够将struct作为匿名字段,自定义类型、内置类型都可以作为匿名字段,而且可以在相应的字段上面进行函数操作(如例子中的append)。

这里有一个问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢?

Go里面很简单的解决了这个问题,最外层的优先访问,也就是当通过student.phone访问的时候,是访问student里面的字段,而不是human里面的字段。

这样就允许去重载通过匿名字段继承的一些字段,当然如果想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。请看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
type Human struct {
name string
age int
phone string // Human类型拥有的字段
}
type Employee struct {
Human // 匿名字段Human
speciality string
phone string // 雇员的phone字段
}
func main() {
Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
fmt.Println("Bob's work phone is:", Bob.phone)
// 如果要访问Human的phone字段
fmt.Println("Bob's personal phone is:", Bob.Human.phone)
}

method

函数的另一种形态,带有接收者的函数,称为method

method

现在假设有这么一个场景,定义了一个struct叫做长方形,现在想要计算他的面积,那么按照一般的思路应该会用下面的方式来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
type Rectangle struct {
width, height float64
}
func area(r Rectangle) float64 {
return r.width*r.height
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
fmt.Println("Area of r1 is: ", area(r1))
fmt.Println("Area of r2 is: ", area(r2))
}

这段代码可以计算出来长方形的面积,但是area()不是作为Rectangle的方法实现的(类似面向对象里面的方法),而是将Rectangle的对象(如r1,r2)作为参数传入函数计算面积的。

这样实现当然没有问题,但是当需要增加圆形、正方形、五边形甚至其它多边形的时候,想计算他们的面积的时候怎么办?那就只能增加新的函数,但是函数名就必须要跟着换了,变成area_rectangle, area_circle, area_triangle...

椭圆代表函数, 而这些函数并不从属于struct(或者以面向对象的术语来说,并不属于class),他们是单独存在于struct外围,而非在概念上属于某个struct的。

很显然,这样的实现并不优雅,并且从概念上来说”面积”是”形状”的一个属性,它是属于这个特定的形状的,就像长方形的长和宽一样。

基于上面的原因所以就有了method的概念,method是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在func后面增加了一个receiver(也就是method所依从的主体)。

用上面提到的形状的例子来说,method area() 是依赖于某个形状(比如说Rectangle)来发生作用的。Rectangle.area()的发出者是Rectangle, area()是属于Rectangle的方法,而非一个外围函数。

更具体地说,Rectangle存在字段 height 和 width, 同时存在方法area(), 这些字段和方法都属于Rectangle。

用Rob Pike的话来说就是:

“A method is a function with an implicit first argument, called a receiver.”

method的语法如下:

1
func (r ReceiverType) funcName(parameters) (results)

下面用最开始的例子用method来实现:

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
package main
import (
"fmt"
"math"
)
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) area() float64 {
return r.width*r.height
}
func (c Circle) area() float64 {
return c.radius * c.radius * math.Pi
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
c1 := Circle{10}
c2 := Circle{25}
fmt.Println("Area of r1 is: ", r1.area())
fmt.Println("Area of r2 is: ", r2.area())
fmt.Println("Area of c1 is: ", c1.area())
fmt.Println("Area of c2 is: ", c2.area())
}

在使用method的时候重要注意几点

  • 虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样

  • method里面可以访问接收者的字段

  • 调用method通过.访问,就像struct里面访问字段一样

在上例,method area() 分别属于Rectangle和Circle, 于是他们的 Receiver 就变成了Rectangle 和 Circle, 或者说,这个area()方法 是由 Rectangle/Circle 发出的。

值得说明的一点是,图示中method用虚线标出,意思是此处方法的Receiver是以值传递,而非引用传递,是的,Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。后文对此会有详细论述。

那是不是method只能作用在struct上面呢?当然不是,他可以定义在任何自定义的类型、内置类型、struct等各种类型上面。什么叫自定义类型,自定义类型不就是struct,其实不是这样的,struct只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。

1
type typeName typeLiteral

请看下面这个申明自定义类型的代码

1
2
3
4
5
6
7
8
9
type ages int
type money float32
type months map[string]int
m := months {
"January":31,
"February":28,
...
"December":31,
}

这样就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于c中的typedef,例如上面ages替代了int,回到method 可以在任何的自定义类型中定义任意多的method,接下来让看一个复杂一点的例子

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main
import "fmt"
const(
WHITE = iota
BLACK
BLUE
RED
YELLOW
)
type Color byte
type Box struct {
width, height, depth float64
color Color
}
type BoxList []Box //a slice of boxes
func (b Box) Volume() float64 {
return b.width * b.height * b.depth
}
func (b *Box) SetColor(c Color) {
b.color = c
}
func (bl BoxList) BiggestColor() Color {
v := 0.00
k := Color(WHITE)
for _, b := range bl {
if bv := b.Volume(); bv > v {
v = bv
k = b.color
}
}
return k
}
func (bl BoxList) PaintItBlack() {
for i := range bl {
bl[i].SetColor(BLACK)
}
}
func (c Color) String() string {
strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
return strings[c]
}
func main() {
boxes := BoxList {
Box{4, 4, 4, RED},
Box{10, 10, 1, YELLOW},
Box{1, 1, 20, BLACK},
Box{10, 10, 1, BLUE},
Box{10, 30, 1, WHITE},
Box{20, 20, 20, YELLOW},
}
fmt.Printf("We have %d boxes in our set\n", len(boxes))
fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String())
fmt.Println("The biggest one is", boxes.BiggestColor().String())
fmt.Println("Let's paint them all black")
boxes.PaintItBlack()
fmt.Println("The color of the second one is", boxes[1].color.String())
fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
}

上面的代码通过const定义了一些常量,然后定义了一些自定义类型

  • Color作为byte的别名

  • 定义了一个struct:Box,含有三个长宽高字段和一个颜色属性

  • 定义了一个slice:BoxList,含有Box

然后以上面的自定义类型为接收者定义了一些method

  • Volume()定义了接收者为Box,返回Box的容量

  • SetColor(c Color),把Box的颜色改为c

  • BiggestColor()定在在BoxList上面,返回list里面容量最大的颜色

  • PaintItBlack()把BoxList里面所有Box的颜色全部变成黑色

  • String()定义在Color上面,返回Color的具体颜色(字符串格式)

上面的代码通过文字描述出来之后是不是很简单?一般解决问题都是通过问题的描述,去写相应的代码实现。

指针作为receiver

现在让回过头来看看SetColor这个method,它的receiver是一个指向Box的指针,可以使用*Box。

定义SetColor的真正目的是想改变这个Box的颜色,如果不传Box的指针,那么SetColor接受的其实是Box的一个copy,也就是说method内对于颜色值的修改,其实只作用于Box的copy,而不是真正的Box。所以需要传入指针。

这里可以把receiver当作method的第一个参数来看,然后结合前面函数讲解的传值和传引用就不难理解

这里也许会问SetColor函数里面应该这样定义*b.Color=c,而不是b.Color=c,需要读取到指针相应的值。

其实Go里面这两种方式都是正确的,当用指针去访问相应的字段时(虽然指针没有任何的字段),Go知道要通过指针去获取这个值。PaintItBlack里面调用SetColor的时候是不是应该写成(&bl[i]).SetColor(BLACK),因为SetColor的receiver是*Box,而不是Box。这两种方式都可以,因为Go知道receiver是指针,他自动转了。

也就是说:

如果一个method的receiver是*T,可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method

类似的

如果一个method的receiver是T,可以在一个*T类型的变量P上面调用这个method,而不需要 *P去调用这个method

所以不用担心是调用的指针的method还是不是指针的method,Go知道要做的一切,这对于有多年C/C++编程经验的同学来说,真是解决了一个很大的痛苦。

method继承

通过字段的继承的学习,发现Go的一个神奇之处,method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。来看下面这个例子

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
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
//在human上面定义了一个method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}

method重写

上面的例子中,如果Employee想要实现自己的SayHi,怎么办?简单,和匿名字段冲突一样的道理,可以在Employee上面定义一个method,重写了匿名字段的方法。请看下面的例子

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
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
//Human定义method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Employee的method重写Human的method
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}

通过这些内容,可以设计出基本的面向对象的程序了,但是Go里面的面向对象是如此的简单,没有任何的私有、公有关键字,通过大小写来实现(大写开头的为公有,小写开头的为私有),方法也同样适用这个原则。

interface

Go语言里面设计最精妙的应该算interface,它让面向对象,内容组织实现非常的方便

什么是interface

简单的说,interface是一组method签名的组合,通过interface来定义对象的一组行为。

前面例子中StudentEmployee都能SayHi,虽然他们的内部实现不一样,但是那不重要,重要的是他们都能say hi

继续做更多的扩展,StudentEmployee实现另一个方法Sing,然后Student实现方法BorrowMoneyEmployee实现SpendSalary

这样Student实现了三个方法:SayHiSingBorrowMoney;而Employee实现了SayHiSingSpendSalary

上面这些方法的组合称为interface(被对象StudentEmployee实现)。例如StudentEmployee都实现了interfaceSayHiSing,也就是这两个对象是该interface类型。而Employee没有实现这个interface:SayHi、SingBorrowMoney,因为Employee没有实现BorrowMoney这个方法。

interface类型

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段Human
school string
loan float32
}
type Employee struct {
Human //匿名字段Human
company string
money float32
}
//Human对象实现Sayhi方法
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
// Human对象实现Sing方法
func (h *Human) Sing(lyrics string) {
fmt.Println("La la, la la la, la la la la la...", lyrics)
}
//Human对象实现Guzzle方法
func (h *Human) Guzzle(beerStein string) {
fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}
// Employee重载Human的Sayhi方法
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //此句可以分成多行
}
//Student实现BorrowMoney方法
func (s *Student) BorrowMoney(amount float32) {
s.loan += amount // (again and again and...)
}
//Employee实现SpendSalary方法
func (e *Employee) SpendSalary(amount float32) {
e.money -= amount // More vodka please!!! Get me through the day!
}
// 定义interface
type Men interface {
SayHi()
Sing(lyrics string)
Guzzle(beerStein string)
}
type YoungChap interface {
SayHi()
Sing(song string)
BorrowMoney(amount float32)
}
type ElderlyGent interface {
SayHi()
Sing(song string)
SpendSalary(amount float32)
}

通过上面的代码可以知道,interface可以被任意的对象实现。看到上面的Men interface被Human、Student和Employee实现。同理,一个对象可以实现任意多个interface,例如上面的Student实现了Men和YoungChap两个interface。

最后,任意的类型都实现了空interface(这样定义:interface{}),也就是包含0个method的interface。

interface值

那么interface里面到底能存什么值呢?如果定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。例如上面例子中,定义了一个Men interface类型的变量m,那么m里面可以存Human、Student或者Employee值。

因为m能够持有这三种类型的对象,所以可以定义一个包含Men类型元素的slice,这个slice可以被赋予实现了Men接口的任意结构的对象,这个和传统意义上面的slice有所不同。

来看一下下面这个例子:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
loan float32
}
type Employee struct {
Human //匿名字段
company string
money float32
}
//Human实现SayHi方法
func (h Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Human实现Sing方法
func (h Human) Sing(lyrics string) {
fmt.Println("La la la la...", lyrics)
}
//Employee重载Human的SayHi方法
func (e Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone)
}
// Interface Men被Human,Student和Employee实现
// 因为这三个类型都实现了这两个方法
type Men interface {
SayHi()
Sing(lyrics string)
}
func main() {
mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}
//定义Men类型的变量i
var i Men
//i能存储Student
i = mike
fmt.Println("This is Mike, a Student:")
i.SayHi()
i.Sing("November rain")
//i也能存储Employee
i = tom
fmt.Println("This is tom, an Employee:")
i.SayHi()
i.Sing("Born to be wild")
//定义了slice Men
fmt.Println("Let's use a slice of Men and see what happens")
x := make([]Men, 3)
//这三个都是不同类型的元素,但是他们实现了interface同一个接口
x[0], x[1], x[2] = paul, sam, mike
for _, value := range x{
value.SayHi()
}
}

通过上面的代码,发现interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现, Go通过interface实现了duck-typing:即”当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子”。

空interface

空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。

1
2
3
4
5
6
7
// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a可以存储任意类型的数值
a = i
a = s

一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。是不是很有用啊!

interface函数参数

interface的变量可以持有任意实现该interface类型的对象,这给编写函数(包括method)提供了一些额外的思考,是不是可以通过定义interface参数,让函数接受各种类型的参数。

举个例子:fmt.Println是常用的一个函数,是否注意到它可以接受任意类型的数据。打开fmt的源码文件,会看到这样一个定义:

1
2
3
type Stringer interface {
String() string
}

也就是说,任何实现了String方法的类型都能作为参数被fmt.Println调用,来试一试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
"fmt"
"strconv"
)
type Human struct {
name string
age int
phone string
}
// 通过这个方法 Human 实现了 fmt.Stringer
func (h Human) String() string {
return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years - ✆ " +h.phone+"❱"
}
func main() {
Bob := Human{"Bob", 39, "000-7777-XXX"}
fmt.Println("This Human is : ", Bob)
}

现在再回顾一下前面的Box示例,发现Color结构也定义了一个method:String。其实这也是实现了fmt.Stringer这个interface,即如果需要某个类型能被fmt包以特殊的格式输出,就必须实现Stringer这个接口。如果没有实现这个接口,fmt将以默认的方式输出。

1
2
3
//实现同样的功能
fmt.Println("The biggest one is", boxes.BiggestsColor().String())
fmt.Println("The biggest one is", boxes.BiggestsColor())

注:实现了error接口的对象(即实现了Error() string的对象),使用fmt输出时,会调用Error()方法,因此不必再定义String()方法了。

interface变量存储的类型

interface的变量里面可以存储任意类型的数值(该类型实现了interface)。那么怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法:

  • Comma-ok断言

Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。

如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。

通过一个例子来更加深入的理解。

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
package main
import (
"fmt"
"strconv"
)
type Element interface{}
type List [] Element
type Person struct {
name string
age int
}
//定义了String方法,实现了fmt.Stringer
func (p Person) String() string {
return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}
func main() {
list := make(List, 3)
list[0] = 1 // an int
list[1] = "Hello" // a string
list[2] = Person{"Dennis", 70}
for index, element := range list {
if value, ok := element.(int); ok {
fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
} else if value, ok := element.(string); ok {
fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
} else if value, ok := element.(Person); ok {
fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
} else {
fmt.Printf("list[%d] is of a different type\n", index)
}
}
}

是否注意到了多个if里面,if里面允许初始化变量。断言的类型越多,那么if else也就越多,所以才引出了下面要介绍的switch。

  • switch测试

重写上面的这个实现

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
package main
import (
"fmt"
"strconv"
)
type Element interface{}
type List [] Element
type Person struct {
name string
age int
}
//打印
func (p Person) String() string {
return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}
func main() {
list := make(List, 3)
list[0] = 1 //an int
list[1] = "Hello" //a string
list[2] = Person{"Dennis", 70}
for index, element := range list{
switch value := element.(type) {
case int:
fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
case string:
fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
case Person:
fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
default:
fmt.Println("list[%d] is of a different type", index)
}
}
}

这里有一点需要强调的是:element.(type)语法不能在switch外的任何逻辑里面使用,如果要在switch外面判断一个类型就使用comma-ok

嵌入interface

Go里面真正吸引人的是它内置的逻辑语法,就像在学习Struct时学习的匿名字段,那么相同的逻辑引入到interface里面,更加完美了。如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。

可以看到源码包container/heap里面有这样的一个定义

1
2
3
4
5
type Interface interface {
sort.Interface //嵌入字段sort.Interface
Push(x interface{}) //a Push method to push elements into the heap
Pop() interface{} //a Pop elements that pops elements from the heap
}

看到sort.Interface其实就是嵌入字段,把sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法:

1
2
3
4
5
6
7
8
9
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less returns whether the element with index i should sort
// before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}

另一个例子就是io包下面的 io.ReadWriter ,它包含了io包下面的ReaderWriter两个interface

1
2
3
4
5
// io.ReadWriter
type ReadWriter interface {
Reader
Writer
}

反射

Go语言实现了反射,所谓反射就是能检查程序在运行时的状态。一般用到的包是reflect包。如何运用reflect包,官方的这篇文章详细的讲解了reflect包的实现原理,laws of reflection 链接地址为 http://golang.org/doc/articles/laws_of_reflection.html

使用reflect一般分成三步,下面简要的讲解一下:要去反射是一个类型的值(这些值都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。这两种获取方式如下:

1
2
t := reflect.TypeOf(i)    //得到类型的元数据,通过t能获取类型定义里面的所有元素
v := reflect.ValueOf(i) //得到实际的值,通过v获取存储在里面的值,还可以去改变值

转化为reflect对象之后就可以进行一些操作了,也就是将reflect对象转化成相应的值,例如

1
2
tag := t.Elem().Field(0).Tag  //获取定义在struct里面的标签
name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值

获取反射值能返回相应的类型和数值

1
2
3
4
5
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

最后,反射的话,那么反射的字段必须是可修改的,前面学习过传值和传引用,这个里面也是一样的道理。反射的字段必须是可读写的意思是,如果下面这样写,那么会发生错误

1
2
3
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)

如果要修改相应的值,必须这样写

1
2
3
4
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)

并发

Go从语言层面支持了并行。

goroutine

goroutineGo并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutinethread更易用、更高效、更轻便。

goroutine是通过Go的runtime管理的一个线程管理器。goroutine通过go关键字实现了,其实就是一个普通的函数。

1
go hello(a, b, c)

通过关键字go就启动了一个goroutine。来看一个例子

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
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 5; i++ {
runtime.Gosched()
fmt.Println(s)
}
}
func main() {
go say("world") //开一个新的Goroutines执行
say("hello") //当前Goroutines执行
}
// 以上程序执行后将输出:
// hello
// world
// hello
// world
// hello
// world
// hello
// world
// hello

可以看到go关键字很方便的就实现了并发编程。

上面的多个goroutine运行在同一个进程里面,共享内存数据,不过设计上要遵循:不要通过共享来通信,而要通过通信来共享。

runtime.Gosched()表示让CPU把时间片让给别人,下次某个时候继续恢复执行该goroutine

默认情况下,在Go 1.5将标识并发系统线程个数的runtime.GOMAXPROCS的初始值由1改为了运行环境的CPU核数

但在Go 1.5以前调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,需要程序中显式调用 runtime.GOMAXPROCS(n) 告诉调度器同时使用多个线程。GOMAXPROCS 设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。如果n < 1,不会改变当前设置。

channels

goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。那么goroutine之间如何进行数据的通信呢,Go提供了一个很好的通信机制channelchannel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。注意,必须使用make 创建channel

1
2
3
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

channel通过操作符<-来接收和发送数据

1
2
ch <- v    // 发送v到channel ch.
v := <-ch // 从ch中接收数据,并赋值给v

把这些应用到例子中来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
func sum(a []int, c chan int) {
total := 0
for _, v := range a {
total += v
}
c <- total // send total to c
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(a[:len(a)/2], c)
go sum(a[len(a)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x + y)
}

默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。所谓阻塞,也就是如果读取(value := <-ch)它将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,直到数据被读出。无缓冲channel是在多个goroutine之间同步很棒的工具。

Buffered Channels

上面介绍了默认的非缓存类型的channel,不过Go也允许指定channel的缓冲大小,很简单,就是channel可以存储多少元素。ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel 中读取一些元素,腾出空间。

1
ch := make(chan type, value)

value = 0 时,channel 是无缓冲阻塞读写的,当value > 0 时,channel 有缓冲、是非阻塞的,直到写满 value 个元素才阻塞写入。

看一下下面这个例子,可以在自己本机测试一下,修改相应的value值

1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
func main() {
c := make(chan int, 2)//修改2为1就报错,修改2为3可以正常运行
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}
//修改为1报如下的错误:
//fatal error: all goroutines are asleep - deadlock!

Range和Close

上面这个例子中,需要读取两次c,这样不是很方便,Go考虑到了这一点,所以也可以通过range,像操作slice或者map一样操作缓存类型的channel,请看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 1, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x + y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}

for i := range c能够不断的读取channel里面的数据,直到该channel被显式的关闭。上面代码看到可以显式的关闭channel,生产者通过内置函数close关闭channel。关闭channel之后就无法再发送任何数据了,在消费方可以通过语法v, ok := <-ch测试channel是否被关闭。如果ok返回false,那么说明channel已经没有任何数据并且已经被关闭。

记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic

另外记住一点的就是channel不像文件之类的,不需要经常去关闭,只有确实没有任何发送数据了,或者想显式的结束range循环之类的

Select

上面介绍的都是只有一个channel的情况,那么如果存在多个channel的时候,该如何操作呢,Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。

select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。

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
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x + y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}

select里面还有default语法,select其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。

1
2
3
4
5
6
select {
case i := <-c:
// use i
default:
// 当c阻塞的时候执行这里
}

超时

有时候会出现goroutine阻塞的情况,那么如何避免整个程序进入阻塞的情况呢?可以利用select来设置超时,通过如下的方式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <- c:
println(v)
case <- time.After(5 * time.Second):
println("timeout")
o <- true
break
}
}
}()
<- o
}

runtime goroutine

runtime包中有几个处理goroutine的函数:

  • Goexit : 退出当前执行的goroutine,但是defer函数还会继续调用

  • Gosched: 让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。

  • NumCPU : 返回 CPU 核数量

  • NumGoroutine: 返回正在执行和排队的任务总数

  • GOMAXPROCS : 用来设置可以并行计算的CPU核数的最大值,并返回之前的值。

错误处理

Go语言主要的设计准则是:简洁、明白,简洁是指语法和C类似,相当的简单,明白是指任何语句都是很明显的,不含有任何隐含的东西,在错误处理方案的设计中也贯彻了这一思想。

在C语言里面是通过返回-1或者NULL之类的信息来表示错误,但是对于使用者来说,不查看相应的API说明文档,根本搞不清楚这个返回值究竟代表什么意思,比如:返回0是成功,还是失败,而Go定义了一个叫做error的类型,来显式表达错误。在使用时,通过把返回的error变量与nil的比较,来判定操作是否成功。例如os.Open函数在打开文件失败时将返回一个不为nilerror变量

1
func Open(name string) (file *File, err error)

下面这个例子通过调用os.Open打开一个文件,如果出现错误,那么就会调用log.Fatal来输出错误信息:

1
2
3
4
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}

类似于os.Open函数,标准包中所有可能出错的API都会返回一个error变量,以方便错误处理,这个小节将详细地介绍error类型的设计,和讨论开发Web应用中如何更好地处理error

Error类型

error类型是一个接口类型,这是它的定义:

1
2
3
type error interface {
Error() string
}

error是一个内置的接口类型,可以在/builtin/包下面找到相应的定义。而在很多内部包里面用到的 errorerrors包下面的实现的私有结构errorString

1
2
3
4
5
6
7
8
// errorString is a trivial implementation of error.
type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

可以通过errors.New把一个字符串转化为errorString,以得到一个满足接口error的对象,其内部实现如下:

1
2
3
4
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}

下面这个例子演示了如何使用errors.New:

1
2
3
4
5
6
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}

在下面的例子中,在调用Sqrt的时候传递的一个负数,然后就得到了non-nilerror对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:

1
2
3
4
f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}

自定义Error

error是一个interface,所以在实现自己的包的时候,通过定义实现此接口的结构,就可以实现自己的错误定义,请看来自Json包的示例:

1
2
3
4
5
6
type SyntaxError struct {
msg string // 错误描述
Offset int64 // 错误发生的位置
}

func (e *SyntaxError) Error() string { return e.msg }

Offset字段在调用Error的时候不会被打印,但可以通过类型断言获取错误类型,然后可以打印相应的错误信息,请看下面的例子:

1
2
3
4
5
6
7
if err := dec.Decode(&val); err != nil {
if serr, ok := err.(*json.SyntaxError); ok {
line, col := findLine(f, serr.Offset)
return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
}
return err
}

需要注意的是,函数返回自定义错误时,返回值推荐设置为error类型,而非自定义错误类型,特别需要注意的是不应预声明自定义错误类型的变量。例如:

1
2
3
4
5
6
7
func Decode() *SyntaxError { // 错误,将可能导致上层调用者err!=nil的判断永远为true。
var err *SyntaxError // 预声明错误变量
if 出错条件 {
err = &SyntaxError{}
}
return err // 错误,err永远等于非nil,导致上层调用者err!=nil的判断始终为true
}

原因见 http://golang.org/doc/faq#nil_error (需科学上网)

上面例子简单的演示了如何自定义Error类型。但是如果还需要更复杂的错误处理呢?此时,来参考一下net包采用的方法:

1
2
3
4
5
6
7
package net

type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}

在调用的地方,通过类型断言err是不是net.Error,来细化错误的处理,例如下面的例子,如果一个网络发生临时性错误,那么将会sleep 1秒之后重试:

1
2
3
4
5
6
7
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}

错误处理

Go在错误处理上采用了与C类似的检查返回值的方式,而不是其他多数主流语言采用的异常方式,这造成了代码编写上的一个很大的缺点:错误处理代码的冗余,对于这种情况是通过复用检测函数来减少类似的代码。

请看下面这个例子代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func init() {
http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
http.Error(w, err.Error(), 500)
return
}
if err := viewTemplate.Execute(w, record); err != nil {
http.Error(w, err.Error(), 500)
}
}

上面的例子中获取数据和模板展示调用时都有检测错误,当有错误发生时,调用了统一的处理函数http.Error,返回给客户端500错误码,并显示相应的错误数据。但是当越来越多的HandleFunc加入之后,这样的错误处理逻辑代码就会越来越多,其实可以通过自定义路由器来缩减代码

1
2
3
4
5
6
7
type appHandler func(http.ResponseWriter, *http.Request) error

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, err.Error(), 500)
}
}

上面定义了自定义的路由器,然后可以通过如下方式来注册函数:

1
2
3
func init() {
http.Handle("/view", appHandler(viewRecord))
}

当请求/view的时候逻辑处理可以变成如下代码,和第一种实现方式相比较已经简单了很多。

1
2
3
4
5
6
7
8
9
func viewRecord(w http.ResponseWriter, r *http.Request) error {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return err
}
return viewTemplate.Execute(w, record)
}

上面的例子错误处理的时候所有的错误返回给用户的都是500错误码,然后打印出来相应的错误代码,其实可以把这个错误信息定义的更加友好,调试的时候也方便定位问题,可以自定义返回的错误类型:

1
2
3
4
5
type appError struct {
Error error
Message string
Code int
}

这样自定义路由器可以改成如下方式:

1
2
3
4
5
6
7
8
9
type appHandler func(http.ResponseWriter, *http.Request) *appError

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil { // e is *appError, not os.Error.
c := appengine.NewContext(r)
c.Errorf("%v", e.Error)
http.Error(w, e.Message, e.Code)
}
}

这样修改完自定义错误之后,逻辑处理可以改成如下方式:

1
2
3
4
5
6
7
8
9
10
11
12
func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return &appError{err, "Record not found", 404}
}
if err := viewTemplate.Execute(w, record); err != nil {
return &appError{err, "Can't display record", 500}
}
return nil
}

如上所示,在访问view的时候可以根据不同的情况获取不同的错误码和错误信息,虽然这个和第一个版本的代码量差不多,但是这个显示的错误更加明显,提示的错误信息更加友好,扩展性也比第一个更好。

总结

在程序设计中,容错是相当重要的一部分工作,在Go中它是通过错误处理来实现的,error虽然只是一个接口,但是其变化却可以有很多,可以根据自己的需求来实现不同的处理。

Golang新手可能会踩的50个坑

前言

Go 是一门简单有趣的编程语言,与其他语言一样,在使用时不免会遇到很多坑,不过它们大多不是 Go 本身的设计缺陷。如果你刚从其他语言转到 Go,那这篇文章里的坑多半会踩到。

如果花时间学习官方 doc、wiki、讨论邮件列表Rob Pike 的大量文章以及 Go 的源码,会发现这篇文章中的坑是很常见的,新手跳过这些坑,能减少大量调试代码的时间。

初级篇:1-34

1. 左大括号 { 不能单独放一行

在其他大多数语言中,{ 的位置你自行决定。Go 比较特别,遵守分号注入规则(automatic semicolon injection):编译器会在每行代码尾部特定分隔符后加 ; 来分隔多条语句,比如会在 ) 后加分号:

1
2
3
4
5
6
7
8
9
10
11
// 错误示例
func main()
{
println("hello world")
}

// 等效于
func main(); // 无函数体
{
println("hello world")
}

./main.go: missing function body

./main.go: syntax error: unexpected semicolon or newline before {

1
2
3
4
// 正确示例
func main() {
println("hello world")
}

2. 未使用的变量

如果在函数体代码中有未使用的变量,则无法通过编译,不过全局变量声明但不使用是可以的。

即使变量声明后为变量赋值,依旧无法通过编译,需在某处使用它:

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
// 错误示例
var gvar int // 全局变量,声明不使用也可以

func main() {
var one int // error: one declared and not used
two := 2 // error: two declared and not used
var three int // error: three declared and not used
three = 3
}

// 正确示例
// 可以直接注释或移除未使用的变量
func main() {
var one int
_ = one

two := 2
println(two)

var three int
one = three

var four int
four = four
}

3. 未使用的 import

如果你 import 一个包,但包中的变量、函数、接口和结构体一个都没有用到的话,将编译失败。

可以使用 _ 下划线符号作为别名来忽略导入的包,从而避免编译错误,这只会执行 package 的 init()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 错误示例
import (
"fmt" // imported and not used: "fmt"
"log" // imported and not used: "log"
"time" // imported and not used: "time"
)

func main() {
}

// 正确示例
// 可以使用 goimports 工具来注释或移除未使用到的包
import (
_ "fmt"
"log"
"time"
)

func main() {
_ = log.Println
_ = time.Now
}

4. 简短声明的变量只能在函数内部使用

1
2
3
4
5
6
7
8
9
// 错误示例
myvar := 1 // syntax error: non-declaration statement outside function body
func main() {
}

// 正确示例
var myvar = 1
func main() {
}

5. 使用简短声明来重复声明变量

不能用简短声明方式来单独为一个变量重复声明, := 左侧至少有一个新变量,才允许多变量的重复声明:

1
2
3
4
5
6
7
8
9
10
11
12
// 错误示例
func main() {
one := 0
one := 1 // error: no new variables on left side of :=
}

// 正确示例
func main() {
one := 0
one, two := 1, 2 // two 是新变量,允许 one 的重复声明。比如 error 处理经常用同名变量 err
one, two = two, one // 交换两个变量值的简写
}

6. 不能使用简短声明来设置字段的值

struct 的变量字段不能使用 := 来赋值以使用预定义的变量来避免解决:

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
// 错误示例
type info struct {
result int
}

func work() (int, error) {
return 3, nil
}

func main() {
var data info
data.result, err := work() // error: non-name data.result on left side of :=
fmt.Printf("info: %+v\n", data)
}

// 正确示例
func main() {
var data info
var err error // err 需要预声明

data.result, err = work()
if err != nil {
fmt.Println(err)
return
}

fmt.Printf("info: %+v\n", data)
}

7. 不小心覆盖了变量

对从动态语言转过来的开发者来说,简短声明很好用,这可能会让人误会 := 是一个赋值操作符。

如果你在新的代码块中像下边这样误用了 :=,编译不会报错,但是变量不会按你的预期工作:

1
2
3
4
5
6
7
8
9
10
func main() {
x := 1
println(x) // 1
{
println(x) // 1
x := 2
println(x) // 2 // 新的 x 变量的作用域只在代码块内部
}
println(x) // 1
}

这是 Go 开发者常犯的错,而且不易被发现。

可使用 vet 工具来诊断这种变量覆盖,Go 默认不做覆盖检查,添加 -shadow 选项来启用:

1
2
> go tool vet -shadow main.go
main.go:9: declaration of "x" shadows declaration at main.go:5

注意 vet 不会报告全部被覆盖的变量,可以使用 go-nyet 来做进一步的检测:

1
2
> $GOPATH/bin/go-nyet main.go
main.go:10:3:Shadowing variable `x`

8. 显式类型的变量无法使用 nil 来初始化

nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值。但声明时不指定类型,编译器也无法推断出变量的具体类型。

1
2
3
4
5
6
7
8
9
10
11
// 错误示例
func main() {
var x = nil // error: use of untyped nil
_ = x
}

// 正确示例
func main() {
var x interface{} = nil
_ = x
}

9. 直接使用值为 nil 的 slice、map

允许对值为 nil 的 slice 添加元素,但对值为 nil 的 map 添加元素则会造成运行时 panic

1
2
3
4
5
6
7
8
9
10
11
12
// map 错误示例
func main() {
var m map[string]int
m["one"] = 1 // error: panic: assignment to entry in nil map
// m := make(map[string]int)// map 的正确声明,分配了实际的内存
}

// slice 正确示例
func main() {
var s []int
s = append(s, 1)
}

10. map 容量

在创建 map 类型的变量时可以指定容量,但不能像 slice 一样使用 cap() 来检测分配空间的大小:

1
2
3
4
5
// 错误示例
func main() {
m := make(map[string]int, 99)
println(cap(m)) // error: invalid argument m1 (type map[string]int) for cap
}

11. string 类型的变量值不能为 nil

对那些喜欢用 nil 初始化字符串的人来说,这就是坑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误示例
func main() {
var s string = nil // cannot use nil as type string in assignment
if s == nil { // invalid operation: s == nil (mismatched types string and nil)
s = "default"
}
}

// 正确示例
func main() {
var s string // 字符串类型的零值是空串 ""
if s == "" {
s = "default"
}
}

12. Array 类型的值作为函数参数

在 C/C++ 中,数组(名)是指针。将数组作为参数传进函数时,相当于传递了数组内存地址的引用,在函数内部会改变该数组的值。

在 Go 中,数组是值。作为参数传进函数时,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的:

1
2
3
4
5
6
7
8
9
10
// 数组使用值拷贝传参
func main() {
x := [3]int{1,2,3}

func(arr [3]int) {
arr[0] = 7
fmt.Println(arr) // [7 2 3]
}(x)
fmt.Println(x) // [1 2 3] // 并不是你以为的 [7 2 3]
}

如果想修改参数数组:

  • 直接传递指向这个数组的指针类型:
1
2
3
4
5
6
7
8
9
10
// 传址会修改原数据
func main() {
x := [3]int{1,2,3}

func(arr *[3]int) {
(*arr)[0] = 7
fmt.Println(arr) // &[7 2 3]
}(&x)
fmt.Println(x) // [7 2 3]
}
  • 直接使用 slice:即使函数内部得到的是 slice 的值拷贝,但依旧会更新 slice 的原始数据(底层 array)
1
2
3
4
5
6
7
8
9
// 会修改 slice 的底层 array,从而修改 slice
func main() {
x := []int{1, 2, 3}
func(arr []int) {
arr[0] = 7
fmt.Println(x) // [7 2 3]
}(x)
fmt.Println(x) // [7 2 3]
}

13. range 遍历 slice 和 array 时混淆了返回值

与其他编程语言中的 for-inforeach 遍历语句不同,Go 中的 range 在遍历时会生成 2 个值,第一个是元素索引,第二个是元素的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误示例
func main() {
x := []string{"a", "b", "c"}
for v := range x {
fmt.Println(v) // 0 1 2
}
}

// 正确示例
func main() {
x := []string{"a", "b", "c"}
for _, v := range x { // 使用 _ 丢弃索引
fmt.Println(v)
}
}

14. slice 和 array 其实是一维数据

看起来 Go 支持多维的 array 和 slice,可以创建数组的数组、切片的切片,但其实并不是。

对依赖动态计算多维数组值的应用来说,就性能和复杂度而言,用 Go 实现的效果并不理想。

可以使用原始的一维数组、“独立“ 的切片、“共享底层数组”的切片来创建动态的多维数组。

  1. 使用原始的一维数组:要做好索引检查、溢出检测、以及当数组满时再添加值时要重新做内存分配。
  2. 使用“独立”的切片分两步:
  • 创建外部 slice

  • 对每个内部 slice 进行内存分配
    注意内部的 slice 相互独立,使得任一内部 slice 增缩都不会影响到其他的 slice

1
2
3
4
5
6
7
8
9
10
// 使用各自独立的 6 个 slice 来创建 [2][3] 的动态多维数组
func main() {
x := 2
y := 4

table := make([][]int, x)
for i := range table {
table[i] = make([]int, y)
}
}
  1. 使用“共享底层数组”的切片
  • 创建一个存放原始数据的容器 slice

  • 创建其他的 slice

  • 切割原始 slice 来初始化其他的 slice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
h, w := 2, 4
raw := make([]int, h*w)

for i := range raw {
raw[i] = i
}

// 初始化原始 slice
fmt.Println(raw, &raw[4]) // [0 1 2 3 4 5 6 7] 0xc420012120

table := make([][]int, h)
for i := range table {

// 等间距切割原始 slice,创建动态多维数组 table
// 0: raw[0*4: 0*4 + 4]
// 1: raw[1*4: 1*4 + 4]
table[i] = raw[i*w : i*w + w]
}

fmt.Println(table, &table[1][0]) // [[0 1 2 3] [4 5 6 7]] 0xc420012120
}

更多关于多维数组的参考

go-how-is-two-dimensional-arrays-memory-representation

what-is-a-concise-way-to-create-a-2d-slice-in-go

15. 访问 map 中不存在的 key

和其他编程语言类似,如果访问了 map 中不存在的 key 则希望能返回 nil,比如在 PHP 中:

1
2
> php -r '$v = ["x"=>1, "y"=>2]; @var_dump($v["z"]);'
NULL

Go 则会返回元素对应数据类型的零值,比如 nil''false 和 0,取值操作总有值返回,故不能通过取出来的值来判断 key 是不是在 map 中。

检查 key 是否存在可以用 map 直接访问,检查返回的第二个参数即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误的 key 检测方式
func main() {
x := map[string]string{"one": "2", "two": "", "three": "3"}
if v := x["two"]; v == "" {
fmt.Println("key two is no entry") // 键 two 存不存在都会返回的空字符串
}
}

// 正确示例
func main() {
x := map[string]string{"one": "2", "two": "", "three": "3"}
if _, ok := x["two"]; !ok {
fmt.Println("key two is no entry")
}
}

16. string 类型的值是常量,不可更改

尝试使用索引遍历字符串,来更新字符串中的个别字符,是不允许的。

string 类型的值是只读的二进制 byte slice,如果真要修改字符串中的字符,将 string 转为 []byte 修改后,再转为 string 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 修改字符串的错误示例
func main() {
x := "text"
x[0] = "T" // error: cannot assign to x[0]
fmt.Println(x)
}

// 修改示例
func main() {
x := "text"
xBytes := []byte(x)
xBytes[0] = 'T' // 注意此时的 T 是 rune 类型
x = string(xBytes)
fmt.Println(x) // Text
}

注意: 上边的示例并不是更新字符串的正确姿势,因为一个 UTF8 编码的字符可能会占多个字节,比如汉字就需要 3~4 个字节来存储,此时更新其中的一个字节是错误的。

更新字串的正确姿势:将 string 转为 rune slice(此时 1 个 rune 可能占多个 byte),直接更新 rune 中的字符

1
2
3
4
5
6
7
func main() {
x := "text"
xRunes := []rune(x)
xRunes[0] = '我'
x = string(xRunes)
fmt.Println(x) // 我ext
}

17. string 与 byte slice 之间的转换

当进行 string 和 byte slice 相互转换时,参与转换的是拷贝的原始值。这种转换的过程,与其他编程语的强制类型转换操作不同,也和新 slice 与旧 slice 共享底层数组不同。

Go 在 string 与 byte slice 相互转换上优化了两点,避免了额外的内存分配:

  • map[string] 中查找 key 时,使用了对应的 []byte,避免做 m[string(key)] 的内存分配
  • 使用 for range 迭代 string 转换为 []byte 的迭代:for i,v := range []byte(str) {...}

雾:参考原文

18. string 与索引操作符

对字符串用索引访问返回的不是字符,而是一个 byte 值。

这种处理方式和其他语言一样,比如 PHP 中:

1
2
3
4
5
6
7
8
> php -r '$name="中文"; var_dump($name);'    # "中文" 占用 6 个字节
string(6) "中文"

> php -r '$name="中文"; var_dump($name[0]);' # 把第一个字节当做 Unicode 字符读取,显示 U+FFFD
string(1) "�"

> php -r '$name="中文"; var_dump($name[0].$name[1].$name[2]);'
string(3) "中"
1
2
3
4
5
func main() {
x := "ascii"
fmt.Println(x[0]) // 97
fmt.Printf("%T\n", x[0])// uint8
}

如果需要使用 for range 迭代访问字符串中的字符(unicode code point / rune),标准库中有 "unicode/utf8" 包来做 UTF8 的相关解码编码。另外 utf8string 也有像 func (s *String) At(i int) rune 等很方便的库函数。

19. 字符串并不都是 UTF8 文本

string 的值不必是 UTF8 文本,可以包含任意的值。只有字符串是文字字面值时才是 UTF8 文本,字串可以通过转义来包含其他数据。

判断字符串是否是 UTF8 文本,可使用 “unicode/utf8” 包中的 ValidString() 函数:

1
2
3
4
5
6
7
8
9
10
func main() {
str1 := "ABC"
fmt.Println(utf8.ValidString(str1)) // true

str2 := "A\xfeC"
fmt.Println(utf8.ValidString(str2)) // false

str3 := "A\\xfeC"
fmt.Println(utf8.ValidString(str3)) // true // 把转义字符转义成字面值
}

20. 字符串的长度

在 Python 中:

1
2
data = u'♥'
print(len(data)) # 1

然而在 Go 中:

1
2
3
4
func main() {
char := "♥"
fmt.Println(len(char)) // 3
}

Go 的内建函数 len() 返回的是字符串的 byte 数量,而不是像 Python 中那样是计算 Unicode 字符数。

如果要得到字符串的字符数,可使用 “unicode/utf8” 包中的 RuneCountInString(str string) (n int)

1
2
3
4
func main() {
char := "♥"
fmt.Println(utf8.RuneCountInString(char)) // 1
}

注意:RuneCountInString 并不总是返回我们看到的字符数,因为有的字符会占用 2 个 rune:

1
2
3
4
5
6
func main() {
char := "é"
fmt.Println(len(char)) // 3
fmt.Println(utf8.RuneCountInString(char)) // 2
fmt.Println("cafe\u0301") // café // 法文的 cafe,实际上是两个 rune 的组合
}

参考:normalization

21. 在多行 array、slice、map 语句中缺少 ,

1
2
3
4
5
6
7
8
9
func main() {
x := []int {
1,
2 // syntax error: unexpected newline, expecting comma or }
}
y := []int{1,2,}
z := []int{1,2}
// ...
}

声明语句中 } 折叠到单行后,尾部的 , 不是必需的。

22. log.Fatallog.Panic 不只是 log

log 标准库提供了不同的日志记录等级,与其他语言的日志库不同,Go 的 log 包在调用 Fatal*()Panic*() 时能做更多日志外的事,如中断程序的执行等:

1
2
3
4
func main() {
log.Fatal("Fatal level log: log entry") // 输出信息后,程序终止执行
log.Println("Nomal level log: log entry")
}

23. 对内建数据结构的操作并不是同步的

尽管 Go 本身有大量的特性来支持并发,但并不保证并发的数据安全,用户需自己保证变量等数据以原子操作更新。

goroutine 和 channel 是进行原子操作的好方法,或使用 “sync” 包中的锁。

24. range 迭代 string 得到的值

range 得到的索引是字符值(Unicode point / rune)第一个字节的位置,与其他编程语言不同,这个索引并不直接是字符在字符串中的位置。

注意一个字符可能占多个 rune,比如法文单词 café 中的 é。操作特殊字符可使用norm 包。

for range 迭代会尝试将 string 翻译为 UTF8 文本,对任何无效的码点都直接使用 0XFFFD rune(�)UNicode 替代字符来表示。如果 string 中有任何非 UTF8 的数据,应将 string 保存为 byte slice 再进行操作。

1
2
3
4
5
6
7
8
9
10
func main() {
data := "A\xfe\x02\xff\x04"
for _, v := range data {
fmt.Printf("%#x ", v) // 0x41 0xfffd 0x2 0xfffd 0x4 // 错误
}

for _, v := range []byte(data) {
fmt.Printf("%#x ", v) // 0x41 0xfe 0x2 0xff 0x4 // 正确
}
}

25. range 迭代 map

如果你希望以特定的顺序(如按 key 排序)来迭代 map,要注意每次迭代都可能产生不一样的结果。

Go 的运行时是有意打乱迭代顺序的,所以你得到的迭代结果可能不一致。但也并不总会打乱,得到连续相同的 5 个迭代结果也是可能的,如:

1
2
3
4
5
6
func main() {
m := map[string]int{"one": 1, "two": 2, "three": 3, "four": 4}
for k, v := range m {
fmt.Println(k, v)
}
}

如果你去 Go Playground 重复运行上边的代码,输出是不会变的,只有你更新代码它才会重新编译。重新编译后迭代顺序是被打乱的:

img

26. switch 中的 fallthrough 语句

switch 语句中的 case 代码块会默认带上 break,但可以使用 fallthrough 来强制执行下一个 case 代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
isSpace := func(char byte) bool {
switch char {
case ' ': // 空格符会直接 break,返回 false // 和其他语言不一样
// fallthrough // 返回 true
case '\t':
return true
}
return false
}
fmt.Println(isSpace('\t')) // true
fmt.Println(isSpace(' ')) // false
}

不过你可以在 case 代码块末尾使用 fallthrough,强制执行下一个 case 代码块。

也可以改写 case 为多条件判断:

1
2
3
4
5
6
7
8
9
10
11
func main() {
isSpace := func(char byte) bool {
switch char {
case ' ', '\t':
return true
}
return false
}
fmt.Println(isSpace('\t')) // true
fmt.Println(isSpace(' ')) // true
}

27. 自增和自减运算

很多编程语言都自带前置后置的 ++-- 运算。但 Go 特立独行,去掉了前置操作,同时 ++ 只作为运算符而非表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误示例
func main() {
data := []int{1, 2, 3}
i := 0
++i // syntax error: unexpected ++, expecting }
fmt.Println(data[i++]) // syntax error: unexpected ++, expecting :
}

// 正确示例
func main() {
data := []int{1, 2, 3}
i := 0
i++
fmt.Println(data[i]) // 2
}

28. 按位取反

很多编程语言使用 ~ 作为一元按位取反(NOT)操作符,Go 重用 ^ XOR 操作符来按位取反:

1
2
3
4
5
6
7
8
9
10
11
// 错误的取反操作
func main() {
fmt.Println(~2) // bitwise complement operator is ^
}

// 正确示例
func main() {
var d uint8 = 2
fmt.Printf("%08b\n", d) // 00000010
fmt.Printf("%08b\n", ^d) // 11111101
}

同时 ^ 也是按位异或(XOR)操作符。

一个操作符能重用两次,是因为一元的 NOT 操作 NOT 0x02,与二元的 XOR 操作 0x22 XOR 0xff 是一致的。

Go 也有特殊的操作符 AND NOT &^ 操作符,不同位才取1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
var a uint8 = 0x82
var b uint8 = 0x02
fmt.Printf("%08b [A]\n", a)
fmt.Printf("%08b [B]\n", b)

fmt.Printf("%08b (NOT B)\n", ^b)
fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n", b, 0xff, b^0xff)

fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n", a, b, a^b)
fmt.Printf("%08b & %08b = %08b [A AND B]\n", a, b, a&b)
fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n", a, b, a&^b)
fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n", a, b, a&(^b))
}
1
2
3
4
5
6
7
8
10000010 [A]
00000010 [B]
11111101 (NOT B)
00000010 ^ 11111111 = 11111101 [B XOR 0xff]
10000010 ^ 00000010 = 10000000 [A XOR B]
10000010 & 00000010 = 00000010 [A AND B]
10000010 &^00000010 = 10000000 [A 'AND NOT' B]
10000010&(^00000010)= 10000000 [A AND (NOT B)]

29. 运算符的优先级

除了位清除(bit clear)操作符,Go 也有很多和其他语言一样的位操作符,但优先级另当别论。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n", 0x2&0x2+0x4) // & 优先 +
//prints: 0x2 & 0x2 + 0x4 -> 0x6
//Go: (0x2 & 0x2) + 0x4
//C++: 0x2 & (0x2 + 0x4) -> 0x2

fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n", 0x2+0x2<<0x1) // << 优先 +
//prints: 0x2 + 0x2 << 0x1 -> 0x6
//Go: 0x2 + (0x2 << 0x1)
//C++: (0x2 + 0x2) << 0x1 -> 0x8

fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n", 0xf|0x2^0x2) // | 优先 ^
//prints: 0xf | 0x2 ^ 0x2 -> 0xd
//Go: (0xf | 0x2) ^ 0x2
//C++: 0xf | (0x2 ^ 0x2) -> 0xf
}

优先级列表:

1
2
3
4
5
6
Precedence    Operator
5 * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 ||

30. 不导出的 struct 字段无法被 encode

以小写字母开头的字段成员是无法被外部直接访问的,所以 struct 在进行 json、xml、gob 等格式的 encode 操作时,这些私有字段会被忽略,导出时得到零值:

1
2
3
4
5
6
7
8
9
10
11
func main() {
in := MyData{1, "two"}
fmt.Printf("%#v\n", in) // main.MyData{One:1, two:"two"}

encoded, _ := json.Marshal(in)
fmt.Println(string(encoded)) // {"One":1} // 私有字段 two 被忽略了

var out MyData
json.Unmarshal(encoded, &out)
fmt.Printf("%#v\n", out) // main.MyData{One:1, two:""}
}

31. 程序退出时还有 goroutine 在执行

程序默认不等所有 goroutine 都执行完才退出,这点需要特别注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 主程序会直接退出
func main() {
workerCount := 2
for i := 0; i < workerCount; i++ {
go doIt(i)
}
time.Sleep(1 * time.Second)
fmt.Println("all done!")
}

func doIt(workerID int) {
fmt.Printf("[%v] is running\n", workerID)
time.Sleep(3 * time.Second) // 模拟 goroutine 正在执行
fmt.Printf("[%v] is done\n", workerID)
}

如下,main() 主程序不等两个 goroutine 执行完就直接退出了:

img

常用解决办法:使用 “WaitGroup” 变量,它会让主程序等待所有 goroutine 执行完毕再退出。

如果你的 goroutine 要做消息的循环处理等耗时操作,可以向它们发送一条 kill 消息来关闭它们。或直接关闭一个它们都等待接收数据的 channel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 等待所有 goroutine 执行完毕
// 进入死锁
func main() {
var wg sync.WaitGroup
done := make(chan struct{})

workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doIt(i, done, wg)
}

close(done)
wg.Wait()
fmt.Println("all done!")
}

func doIt(workerID int, done <-chan struct{}, wg sync.WaitGroup) {
fmt.Printf("[%v] is running\n", workerID)
defer wg.Done()
<-done
fmt.Printf("[%v] is done\n", workerID)
}

执行结果:

img

看起来好像 goroutine 都执行完了,然而报错:

fatal error: all goroutines are asleep - deadlock!

为什么会发生死锁?goroutine 在退出前调用了 wg.Done() ,程序应该正常退出的。

原因是 goroutine 得到的 “WaitGroup” 变量是 var wg WaitGroup 的一份拷贝值,即 doIt() 传参只传值。所以哪怕在每个 goroutine 中都调用了 wg.Done(), 主程序中的 wg 变量并不会受到影响。

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
// 等待所有 goroutine 执行完毕
// 使用传址方式为 WaitGroup 变量传参
// 使用 channel 关闭 goroutine

func main() {
var wg sync.WaitGroup
done := make(chan struct{})
ch := make(chan interface{})

workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doIt(i, ch, done, &wg) // wg 传指针,doIt() 内部会改变 wg 的值
}

for i := 0; i < workerCount; i++ { // 向 ch 中发送数据,关闭 goroutine
ch <- i
}

close(done)
wg.Wait()
close(ch)
fmt.Println("all done!")
}

func doIt(workerID int, ch <-chan interface{}, done <-chan struct{}, wg *sync.WaitGroup) {
fmt.Printf("[%v] is running\n", workerID)
defer wg.Done()
for {
select {
case m := <-ch:
fmt.Printf("[%v] m => %v\n", workerID, m)
case <-done:
fmt.Printf("[%v] is done\n", workerID)
return
}
}
}

运行效果:

img

32. 向无缓冲的 channel 发送数据,只要 receiver 准备好了就会立刻返回

只有在数据被 receiver 处理时,sender 才会阻塞。因运行环境而异,在 sender 发送完数据后,receiver 的 goroutine 可能没有足够的时间处理下一个数据。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
ch := make(chan string)

go func() {
for m := range ch {
fmt.Println("Processed:", m)
time.Sleep(1 * time.Second) // 模拟需要长时间运行的操作
}
}()

ch <- "cmd.1"
ch <- "cmd.2" // 不会被接收处理
}

运行效果:

img

33. 向已关闭的 channel 发送数据会造成 panic

从已关闭的 channel 接收数据是安全的:

接收状态值 okfalse 时表明 channel 中已没有数据可以接收了。类似的,从有缓冲的 channel 中接收数据,缓存的数据获取完再没有数据可取时,状态值也是 false

向已关闭的 channel 中发送数据会造成 panic:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
ch := make(chan int)
for i := 0; i < 3; i++ {
go func(idx int) {
ch <- idx
}(i)
}

fmt.Println(<-ch) // 输出第一个发送的值
close(ch) // 不能关闭,还有其他的 sender
time.Sleep(2 * time.Second) // 模拟做其他的操作
}

运行结果:

img

针对上边有 bug 的这个例子,可使用一个废弃 channel done 来告诉剩余的 goroutine 无需再向 ch 发送数据。此时 <- done 的结果是 {}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
ch := make(chan int)
done := make(chan struct{})

for i := 0; i < 3; i++ {
go func(idx int) {
select {
case ch <- (idx + 1) * 2:
fmt.Println(idx, "Send result")
case <-done:
fmt.Println(idx, "Exiting")
}
}(i)
}

fmt.Println("Result: ", <-ch)
close(done)
time.Sleep(3 * time.Second)
}

运行效果:

img

34. 使用了值为 nil 的 channel

在一个值为 nil 的 channel 上发送和接收数据将永久阻塞:

1
2
3
4
5
6
7
8
9
10
11
func main() {
var ch chan int // 未初始化,值为 nil
for i := 0; i < 3; i++ {
go func(i int) {
ch <- i
}(i)
}

fmt.Println("Result: ", <-ch)
time.Sleep(2 * time.Second)
}

runtime 死锁错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive (nil chan)]

利用这个死锁的特性,可以用在 select 中动态的打开和关闭 case 语句块:

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
func main() {
inCh := make(chan int)
outCh := make(chan int)

go func() {
var in <-chan int = inCh
var out chan<- int
var val int

for {
select {
case out <- val:
println("--------")
out = nil
in = inCh
case val = <-in:
println("++++++++++")
out = outCh
in = nil
}
}
}()

go func() {
for r := range outCh {
fmt.Println("Result: ", r)
}
}()

time.Sleep(0)
inCh <- 1
inCh <- 2
time.Sleep(3 * time.Second)
}

运行效果:

img

34. 若函数 receiver 传参是传值方式,则无法修改参数的原有值

方法 receiver 的参数与一般函数的参数类似:如果声明为值,那方法体得到的是一份参数的值拷贝,此时对参数的任何修改都不会对原有值产生影响。

除非 receiver 参数是 map 或 slice 类型的变量,并且是以指针方式更新 map 中的字段、slice 中的元素的,才会更新原有值:

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
type data struct {
num int
key *string
items map[string]bool
}

func (this *data) pointerFunc() {
this.num = 7
}

func (this data) valueFunc() {
this.num = 8
*this.key = "valueFunc.key"
this.items["valueFunc"] = true
}

func main() {
key := "key1"

d := data{1, &key, make(map[string]bool)}
fmt.Printf("num=%v key=%v items=%v\n", d.num, *d.key, d.items)

d.pointerFunc() // 修改 num 的值为 7
fmt.Printf("num=%v key=%v items=%v\n", d.num, *d.key, d.items)

d.valueFunc() // 修改 key 和 items 的值
fmt.Printf("num=%v key=%v items=%v\n", d.num, *d.key, d.items)
}

运行结果:

img

中级篇:35-50

35. 关闭 HTTP 的响应体

使用 HTTP 标准库发起请求、获取响应时,即使你不从响应中读取任何数据或响应为空,都需要手动关闭响应体。新手很容易忘记手动关闭,或者写在了错误的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 请求失败造成 panic
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
defer resp.Body.Close() // resp 可能为 nil,不能读取 Body
if err != nil {
fmt.Println(err)
return
}

body, err := ioutil.ReadAll(resp.Body)
checkError(err)

fmt.Println(string(body))
}

func checkError(err error) {
if err != nil{
log.Fatalln(err)
}
}

上边的代码能正确发起请求,但是一旦请求失败,变量 resp 值为 nil,造成 panic:

panic: runtime error: invalid memory address or nil pointer dereference

应该先检查HTTP 响应错误为 nil,再调用 resp.Body.Close() 来关闭响应体:

1
2
3
4
5
6
7
8
9
10
11
// 大多数情况正确的示例
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
checkError(err)

defer resp.Body.Close() // 绝大多数情况下的正确关闭方式
body, err := ioutil.ReadAll(resp.Body)
checkError(err)

fmt.Println(string(body))
}

输出:

Get https://api.ipify.org?format=...: x509: certificate signed by unknown authority

绝大多数请求失败的情况下,resp 的值为 nilerrnon-nil。但如果你得到的是重定向错误,那它俩的值都是 non-nil,最后依旧可能发生内存泄露。2 个解决办法:

  • 可以直接在处理 HTTP 响应错误的代码块中,直接关闭非 nil 的响应体。
  • 手动调用 defer 来关闭响应体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 正确示例
func main() {
resp, err := http.Get("http://www.baidu.com")

// 关闭 resp.Body 的正确姿势
if resp != nil {
defer resp.Body.Close()
}

checkError(err)
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
checkError(err)

fmt.Println(string(body))
}

resp.Body.Close() 早先版本的实现是读取响应体的数据之后丢弃,保证了 keep-alive 的 HTTP 连接能重用处理不止一个请求。但 Go 的最新版本将读取并丢弃数据的任务交给了用户,如果你不处理,HTTP 连接可能会直接关闭而非重用,参考在 Go 1.5 版本文档。

如果程序大量重用 HTTP 长连接,你可能要在处理响应的逻辑代码中加入:

1
_, err = io.Copy(ioutil.Discard, resp.Body)    // 手动丢弃读取完毕的数据

如果你需要完整读取响应,上边的代码是需要写的。比如在解码 API 的 JSON 响应数据:

1
json.NewDecoder(resp.Body).Decode(&data)

36. 关闭 HTTP 连接

一些支持 HTTP1.1 或 HTTP1.0 配置了 connection: keep-alive 选项的服务器会保持一段时间的长连接。但标准库 “net/http” 的连接默认只在服务器主动要求关闭时才断开,所以你的程序可能会消耗完 socket 描述符。解决办法有 2 个,请求结束后:

  • 直接设置请求变量的 Close 字段值为 true,每次请求结束后就会主动关闭连接。
  • 设置 Header 请求头部选项 Connection: close,然后服务器返回的响应头部也会有这个选项,此时 HTTP 标准库会主动断开连接。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 主动关闭连接
func main() {
req, err := http.NewRequest("GET", "http://golang.org", nil)
checkError(err)

req.Close = true
//req.Header.Add("Connection", "close") // 等效的关闭方式

resp, err := http.DefaultClient.Do(req)
if resp != nil {
defer resp.Body.Close()
}
checkError(err)

body, err := ioutil.ReadAll(resp.Body)
checkError(err)

fmt.Println(string(body))
}

你可以创建一个自定义配置的 HTTP transport 客户端,用来取消 HTTP 全局的复用连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
tr := http.Transport{DisableKeepAlives: true}
client := http.Client{Transport: &tr}

resp, err := client.Get("https://golang.google.cn/")
if resp != nil {
defer resp.Body.Close()
}
checkError(err)

fmt.Println(resp.StatusCode) // 200

body, err := ioutil.ReadAll(resp.Body)
checkError(err)

fmt.Println(len(string(body)))
}

根据需求选择使用场景:

  • 若你的程序要向同一服务器发大量请求,使用默认的保持长连接。
  • 若你的程序要连接大量的服务器,且每台服务器只请求一两次,那收到请求后直接关闭连接。或增加最大文件打开数 fs.file-max 的值。

37. 将 JSON 中的数字解码为 interface 类型

在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理,比如下边的代码会造成 panic:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}

if err := json.Unmarshal(data, &result); err != nil {
log.Fatalln(err)
}

fmt.Printf("%T\n", result["status"]) // float64
var status = result["status"].(int) // 类型断言错误
fmt.Println("Status value: ", status)
}

panic: interface conversion: interface {} is float64, not int

如果你尝试 decode 的 JSON 字段是整型,你可以:

  • 将 int 值转为 float 统一使用
  • 将 decode 后需要的 float 值转为 int 使用
1
2
3
4
5
6
7
8
9
10
11
12
// 将 decode 的值转为 int 使用
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}

if err := json.Unmarshal(data, &result); err != nil {
log.Fatalln(err)
}

var status = uint64(result["status"].(float64))
fmt.Println("Status value: ", status)
}
  • 使用 Decoder 类型来 decode JSON 数据,明确表示字段的值类型
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
// 指定字段类型
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}

var decoder = json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()

if err := decoder.Decode(&result); err != nil {
log.Fatalln(err)
}

var status, _ = result["status"].(json.Number).Int64()
fmt.Println("Status value: ", status)
}

// 你可以使用 string 来存储数值数据,在 decode 时再决定按 int 还是 float 使用
// 将数据转为 decode 为 string
func main() {
var data = []byte({"status": 200})
var result map[string]interface{}
var decoder = json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := decoder.Decode(&result); err != nil {
log.Fatalln(err)
}
var status uint64
err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status);
checkError(err)
fmt.Println("Status value: ", status)
}

- 使用 struct 类型将你需要的数据映射为数值型

1
2
3
4
5
6
7
8
9
10
11
// struct 中指定字段类型
func main() {
var data = []byte(`{"status": 200}`)
var result struct {
Status uint64 `json:"status"`
}

err := json.NewDecoder(bytes.NewReader(data)).Decode(&result)
checkError(err)
fmt.Printf("Result: %+v", result)
}
  • 可以使用 struct 将数值类型映射为 json.RawMessage 原生数据类型
    适用于如果 JSON 数据不着急 decode 或 JSON 某个字段的值类型不固定等情况:
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
// 状态名称可能是 int 也可能是 string,指定为 json.RawMessage 类型
func main() {
records := [][]byte{
[]byte(`{"status":200, "tag":"one"}`),
[]byte(`{"status":"ok", "tag":"two"}`),
}

for idx, record := range records {
var result struct {
StatusCode uint64
StatusName string
Status json.RawMessage `json:"status"`
Tag string `json:"tag"`
}

err := json.NewDecoder(bytes.NewReader(record)).Decode(&result)
checkError(err)

var name string
err = json.Unmarshal(result.Status, &name)
if err == nil {
result.StatusName = name
}

var code uint64
err = json.Unmarshal(result.Status, &code)
if err == nil {
result.StatusCode = code
}

fmt.Printf("[%v] result => %+v\n", idx, result)
}
}

38. struct、array、slice 和 map 的值比较

可以使用相等运算符 == 来比较结构体变量,前提是两个结构体的成员都是可比较的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type data struct {
num int
fp float32
complex complex64
str string
char rune
yes bool
events <-chan string
handler interface{}
ref *byte
raw [10]byte
}

func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2: ", v1 == v2) // true
}

如果两个结构体中有任意成员是不可比较的,将会造成编译错误。注意数组成员只有在数组元素可比较时候才可比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type data struct {
num int
checks [10]func() bool // 无法比较
doIt func() bool // 无法比较
m map[string]string // 无法比较
bytes []byte // 无法比较
}

func main() {
v1 := data{}
v2 := data{}

fmt.Println("v1 == v2: ", v1 == v2)
}

invalid operation: v1 == v2 (struct containing [10]func() bool cannot be compared)

Go 提供了一些库函数来比较那些无法使用 == 比较的变量,比如使用 “reflect” 包的 DeepEqual()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 比较相等运算符无法比较的元素
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2)) // true

m1 := map[string]string{"one": "a", "two": "b"}
m2 := map[string]string{"two": "b", "one": "a"}
fmt.Println("v1 == v2: ", reflect.DeepEqual(m1, m2)) // true

s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
// 注意两个 slice 相等,值和顺序必须一致
fmt.Println("v1 == v2: ", reflect.DeepEqual(s1, s2)) // true
}

这种比较方式可能比较慢,根据你的程序需求来使用。DeepEqual() 还有其他用法:

1
2
3
4
5
func main() {
var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2: ", reflect.DeepEqual(b1, b2)) // false
}

注意:

  • DeepEqual() 并不总适合于比较 slice
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
var str = "one"
var in interface{} = "one"
fmt.Println("str == in: ", reflect.DeepEqual(str, in)) // true

v1 := []string{"one", "two"}
v2 := []string{"two", "one"}
fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2)) // false

data := map[string]interface{}{
"code": 200,
"value": []string{"one", "two"},
}
encoded, _ := json.Marshal(data)
var decoded map[string]interface{}
json.Unmarshal(encoded, &decoded)
fmt.Println("data == decoded: ", reflect.DeepEqual(data, decoded)) // false
}

如果要大小写不敏感来比较 byte 或 string 中的英文文本,可以使用 “bytes” 或 “strings” 包的 ToUpper()ToLower() 函数。比较其他语言的 byte 或 string,应使用 bytes.EqualFold()strings.EqualFold()

如果 byte slice 中含有验证用户身份的数据(密文哈希、token 等),不应再使用 reflect.DeepEqual()bytes.Equal()bytes.Compare()。这三个函数容易对程序造成 timing attacks,此时应使用 “crypto/subtle” 包中的 subtle.ConstantTimeCompare() 等函数

  • reflect.DeepEqual() 认为空 slice 与 nil slice 并不相等,但注意 byte.Equal() 会认为二者相等:
1
2
3
4
5
6
7
8
func main() {
var b1 []byte = nil
b2 := []byte{}

// b1 与 b2 长度相等、有相同的字节序
// nil 与 slice 在字节上是相同的
fmt.Println("b1 == b2: ", bytes.Equal(b1, b2)) // true
}

39. 从 panic 中恢复

在一个 defer 延迟执行的函数中调用 recover() ,它便能捕捉 / 中断 panic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误的 recover 调用示例
func main() {
recover() // 什么都不会捕捉
panic("not good") // 发生 panic,主程序退出
recover() // 不会被执行
println("ok")
}

// 正确的 recover 调用示例
func main() {
defer func() {
fmt.Println("recovered: ", recover())
}()
panic("not good")
}

从上边可以看出,recover() 仅在 defer 执行的函数中调用才会生效。

1
2
3
4
5
6
7
8
9
10
11
// 错误的调用示例
func main() {
defer func() {
doRecover()
}()
panic("not good")
}

func doRecover() {
fmt.Println("recobered: ", recover())
}

recobered: panic: not good

40. 在 range 迭代 slice、array、map 时通过更新引用来更新元素

在 range 迭代中,得到的值其实是元素的一份值拷贝,更新拷贝并不会更改原来的元素,即是拷贝的地址并不是原有元素的地址:

1
2
3
4
5
6
7
func main() {
data := []int{1, 2, 3}
for _, v := range data {
v *= 10 // data 中原有元素是不会被修改的
}
fmt.Println("data: ", data) // data: [1 2 3]
}

如果要修改原有元素的值,应该使用索引直接访问:

1
2
3
4
5
6
7
func main() {
data := []int{1, 2, 3}
for i, v := range data {
data[i] = v * 10
}
fmt.Println("data: ", data) // data: [10 20 30]
}

如果你的集合保存的是指向值的指针,需稍作修改。依旧需要使用索引访问元素,不过可以使用 range 出来的元素直接更新原有值:

1
2
3
4
5
6
7
func main() {
data := []*struct{ num int }{{1}, {2}, {3},}
for _, v := range data {
v.num *= 10 // 直接使用指针更新
}
fmt.Println(data[0], data[1], data[2]) // &{10} &{20} &{30}
}

41. slice 中隐藏的数据

从 slice 中重新切出新 slice 时,新 slice 会引用原 slice 的底层数组。如果跳了这个坑,程序可能会分配大量的临时 slice 来指向原底层数组的部分数据,将导致难以预料的内存使用。

1
2
3
4
5
6
7
8
9
10
func get() []byte {
raw := make([]byte, 10000)
fmt.Println(len(raw), cap(raw), &raw[0]) // 10000 10000 0xc420080000
return raw[:3] // 重新分配容量为 10000 的 slice
}

func main() {
data := get()
fmt.Println(len(data), cap(data), &data[0]) // 3 10000 0xc420080000
}

可以通过拷贝临时 slice 的数据,而不是重新切片来解决:

1
2
3
4
5
6
7
8
9
10
11
12
func get() (res []byte) {
raw := make([]byte, 10000)
fmt.Println(len(raw), cap(raw), &raw[0]) // 10000 10000 0xc420080000
res = make([]byte, 3)
copy(res, raw[:3])
return
}

func main() {
data := get()
fmt.Println(len(data), cap(data), &data[0]) // 3 3 0xc4200160b8
}

42. Slice 中数据的误用

举个简单例子,重写文件路径(存储在 slice 中)

分割路径来指向每个不同级的目录,修改第一个目录名再重组子目录名,创建新路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 错误使用 slice 的拼接示例
func main() {
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path, '/') // 4
println(sepIndex)

dir1 := path[:sepIndex]
dir2 := path[sepIndex+1:]
println("dir1: ", string(dir1)) // AAAA
println("dir2: ", string(dir2)) // BBBBBBBBB

dir1 = append(dir1, "suffix"...)
println("current path: ", string(path)) // AAAAsuffixBBBB

path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})
println("dir1: ", string(dir1)) // AAAAsuffix
println("dir2: ", string(dir2)) // uffixBBBB

println("new path: ", string(path)) // AAAAsuffix/uffixBBBB // 错误结果
}

拼接的结果不是正确的 AAAAsuffix/BBBBBBBBB,因为 dir1、 dir2 两个 slice 引用的数据都是 path 的底层数组,第 13 行修改 dir1 同时也修改了 path,也导致了 dir2 的修改

解决方法:

  • 重新分配新的 slice 并拷贝你需要的数据
  • 使用完整的 slice 表达式:input[low:high:max],容量便调整为 max - low
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用 full slice expression
func main() {

path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path, '/') // 4
dir1 := path[:sepIndex:sepIndex] // 此时 cap(dir1) 指定为4, 而不是先前的 16
dir2 := path[sepIndex+1:]
dir1 = append(dir1, "suffix"...)

path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})
println("dir1: ", string(dir1)) // AAAAsuffix
println("dir2: ", string(dir2)) // BBBBBBBBB
println("new path: ", string(path)) // AAAAsuffix/BBBBBBBBB
}

第 6 行中第三个参数是用来控制 dir1 的新容量,再往 dir1 中 append 超额元素时,将分配新的 buffer 来保存。而不是覆盖原来的 path 底层数组

43. 旧 slice

当你从一个已存在的 slice 创建新 slice 时,二者的数据指向相同的底层数组。如果你的程序使用这个特性,那需要注意 “旧”(stale) slice 问题。

某些情况下,向一个 slice 中追加元素而它指向的底层数组容量不足时,将会重新分配一个新数组来存储数据。而其他 slice 还指向原来的旧底层数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 超过容量将重新分配数组来拷贝值、重新存储
func main() {
s1 := []int{1, 2, 3}
fmt.Println(len(s1), cap(s1), s1) // 3 3 [1 2 3 ]

s2 := s1[1:]
fmt.Println(len(s2), cap(s2), s2) // 2 2 [2 3]

for i := range s2 {
s2[i] += 20
}
// 此时的 s1 与 s2 是指向同一个底层数组的
fmt.Println(s1) // [1 22 23]
fmt.Println(s2) // [22 23]

s2 = append(s2, 4) // 向容量为 2 的 s2 中再追加元素,此时将分配新数组来存

for i := range s2 {
s2[i] += 10
}
fmt.Println(s1) // [1 22 23] // 此时的 s1 不再更新,为旧数据
fmt.Println(s2) // [32 33 14]
}

44. 类型声明与方法

从一个现有的非 interface 类型创建新类型时,并不会继承原有的方法:

1
2
3
4
5
6
7
8
// 定义 Mutex 的自定义类型
type myMutex sync.Mutex

func main() {
var mtx myMutex
mtx.Lock()
mtx.UnLock()
}

mtx.Lock undefined (type myMutex has no field or method Lock)…

如果你需要使用原类型的方法,可将原类型以匿名字段的形式嵌到你定义的新 struct 中:

1
2
3
4
5
6
7
8
9
10
// 类型以字段形式直接嵌入
type myLocker struct {
sync.Mutex
}

func main() {
var locker myLocker
locker.Lock()
locker.Unlock()
}

interface 类型声明也保留它的方法集:

1
2
3
4
5
6
7
type myLocker sync.Locker

func main() {
var locker myLocker
locker.Lock()
locker.Unlock()
}

45. 跳出 for-switch 和 for-select 代码块

没有指定标签的 break 只会跳出 switch/select 语句,若不能使用 return 语句跳出的话,可为 break 跳出标签指定的代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
// break 配合 label 跳出指定代码块
func main() {
loop:
for {
switch {
case true:
fmt.Println("breaking out...")
//break // 死循环,一直打印 breaking out...
break loop
}
}
fmt.Println("out...")
}

goto 虽然也能跳转到指定位置,但依旧会再次进入 for-switch,死循环。

46. for 语句中的迭代变量与闭包函数

for 语句中的迭代变量在每次迭代中都会重用,即 for 中创建的闭包函数接收到的参数始终是同一个变量,在 goroutine 开始执行时都会得到同一个迭代值:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
data := []string{"one", "two", "three"}

for _, v := range data {
go func() {
fmt.Println(v)
}()
}

time.Sleep(3 * time.Second)
// 输出 three three three
}

最简单的解决方法:无需修改 goroutine 函数,在 for 内部使用局部变量保存迭代值,再传参:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
data := []string{"one", "two", "three"}

for _, v := range data {
vCopy := v
go func() {
fmt.Println(vCopy)
}()
}

time.Sleep(3 * time.Second)
// 输出 one two three
}

另一个解决方法:直接将当前的迭代值以参数形式传递给匿名函数:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
data := []string{"one", "two", "three"}

for _, v := range data {
go func(in string) {
fmt.Println(in)
}(v)
}

time.Sleep(3 * time.Second)
// 输出 one two three
}

注意下边这个稍复杂的 3 个示例区别:

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
type field struct {
name string
}

func (p *field) print() {
fmt.Println(p.name)
}

// 错误示例
func main() {
data := []field{{"one"}, {"two"}, {"three"}}
for _, v := range data {
go v.print()
}
time.Sleep(3 * time.Second)
// 输出 three three three
}

// 正确示例
func main() {
data := []field{{"one"}, {"two"}, {"three"}}
for _, v := range data {
v := v
go v.print()
}
time.Sleep(3 * time.Second)
// 输出 one two three
}

// 正确示例
func main() {
data := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data { // 此时迭代值 v 是三个元素值的地址,每次 v 指向的值不同
go v.print()
}
time.Sleep(3 * time.Second)
// 输出 one two three
}

47. defer 函数的参数值

对 defer 延迟执行的函数,它的参数会在声明时候就会求出具体值,而不是在执行时才求值:

1
2
3
4
5
6
// 在 defer 函数中参数会提前求值
func main() {
var i = 1
defer fmt.Println("result: ", func() int { return i * 2 }())
i++
}

result: 2

48. defer 函数的执行时机

对 defer 延迟执行的函数,会在调用它的函数结束时执行,而不是在调用它的语句块结束时执行,注意区分开。

比如在一个长时间执行的函数里,内部 for 循环中使用 defer 来清理每次迭代产生的资源调用,就会出现问题:

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
// 命令行参数指定目录名
// 遍历读取目录下的文件
func main() {

if len(os.Args) != 2 {
os.Exit(1)
}

dir := os.Args[1]
start, err := os.Stat(dir)
if err != nil || !start.IsDir() {
os.Exit(2)
}

var targets []string
filepath.Walk(dir, func(fPath string, fInfo os.FileInfo, err error) error {
if err != nil {
return err
}

if !fInfo.Mode().IsRegular() {
return nil
}

targets = append(targets, fPath)
return nil
})

for _, target := range targets {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:", target, "error:", err) //error:too many open files
break
}
defer f.Close() // 在每次 for 语句块结束时,不会关闭文件资源

// 使用 f 资源
}
}

先创建 10000 个文件:

1
2
3
4
#!/bin/bash
for n in {1..10000}; do
echo content > "file${n}.txt"
done

运行效果:

img

解决办法:defer 延迟执行的函数写入匿名函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 目录遍历正常
func main() {
// ...

for _, target := range targets {
func() {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:", target, "error:", err)
return // 在匿名函数内使用 return 代替 break 即可
}
defer f.Close() // 匿名函数执行结束,调用关闭文件资源

// 使用 f 资源
}()
}
}

当然你也可以去掉 defer,在文件资源使用完毕后,直接调用 f.Close() 来关闭。

49. 失败的类型断言

在类型断言语句中,断言失败则会返回目标类型的“零值”,断言变量与原来变量混用可能出现异常情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 错误示例
func main() {
var data interface{} = "great"

// data 混用
if data, ok := data.(int); ok {
fmt.Println("[is an int], data: ", data)
} else {
fmt.Println("[not an int], data: ", data) // [isn't a int], data: 0
}
}

// 正确示例
func main() {
var data interface{} = "great"

if res, ok := data.(int); ok {
fmt.Println("[is an int], data: ", res)
} else {
fmt.Println("[not an int], data: ", data) // [not an int], data: great
}
}

50. 阻塞的 gorutinue 与资源泄露

在 2012 年 Google I/O 大会上,Rob Pike 的 Go Concurrency Patterns 演讲讨论 Go 的几种基本并发模式,如 完整代码 中从数据集中获取第一条数据的函数:

1
2
3
4
5
6
7
8
func First(query string, replicas []Search) Result {
c := make(chan Result)
replicaSearch := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go replicaSearch(i)
}
return <-c
}

在搜索重复时依旧每次都起一个 goroutine 去处理,每个 goroutine 都把它的搜索结果发送到结果 channel 中,channel 中收到的第一条数据会直接返回。

返回完第一条数据后,其他 goroutine 的搜索结果怎么处理?他们自己的协程如何处理?

First() 中的结果 channel 是无缓冲的,这意味着只有第一个 goroutine 能返回,由于没有 receiver,其他的 goroutine 会在发送上一直阻塞。如果你大量调用,则可能造成资源泄露。

为避免泄露,你应该确保所有的 goroutine 都能正确退出,有 2 个解决方法:

  • 使用带缓冲的 channel,确保能接收全部 goroutine 的返回结果:
1
2
3
4
5
6
7
8
func First(query string, replicas ...Search) Result {
c := make(chan Result,len(replicas))
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
  • 使用 select 语句,配合能保存一个缓冲值的 channel default 语句:
    default 的缓冲 channel 保证了即使结果 channel 收不到数据,也不会阻塞 goroutine
1
2
3
4
5
6
7
8
9
10
11
12
13
func First(query string, replicas ...Search) Result {
c := make(chan Result,1)
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
default:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}
  • 使用特殊的废弃(cancellation) channel 来中断剩余 goroutine 的执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func First(query string, replicas ...Search) Result {
c := make(chan Result)
done := make(chan struct{})
defer close(done)
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
case <- done:
}
}
for i := range replicas {
go searchReplica(i)
}

return <-c
}

Rob Pike 为了简化演示,没有提及演讲代码中存在的这些问题。不过对于新手来说,可能会不加思考直接使用。

高级篇:51-57

51. 使用指针作为方法的 receiver

只要值是可寻址的,就可以在值上直接调用指针方法。即是对一个方法,它的 receiver 是指针就足矣。

但不是所有值都是可寻址的,比如 map 类型的元素、通过 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
type data struct {
name string
}

type printer interface {
print()
}

func (p *data) print() {
fmt.Println("name: ", p.name)
}

func main() {
d1 := data{"one"}
d1.print() // d1 变量可寻址,可直接调用指针 receiver 的方法

var in printer = data{"two"}
in.print() // 类型不匹配

m := map[string]data{
"x": data{"three"},
}
m["x"].print() // m["x"] 是不可寻址的 // 变动频繁
}

cannot use data literal (type data) as type printer in assignment:

data does not implement printer (print method has pointer receiver)

cannot call pointer method on m[“x”]

cannot take the address of m[“x”]

52. 更新 map 字段的值

如果 map 一个字段的值是 struct 类型,则无法直接更新该 struct 的单个字段:

1
2
3
4
5
6
7
8
9
10
11
// 无法直接更新 struct 的字段值
type data struct {
name string
}

func main() {
m := map[string]data{
"x": {"Tom"},
}
m["x"].name = "Jerry"
}

cannot assign to struct field m[“x”].name in map

因为 map 中的元素是不可寻址的。需区分开的是,slice 的元素可寻址:

1
2
3
4
5
6
7
8
9
type data struct {
name string
}

func main() {
s := []data{{"Tom"}}
s[0].name = "Jerry"
fmt.Println(s) // [{Jerry}]
}

注意:不久前 gccgo 编译器可更新 map struct 元素的字段值,不过很快便修复了,官方认为是 Go1.3 的潜在特性,无需及时实现,依旧在 todo list 中。

更新 map 中 struct 元素的字段值,有 2 个方法:

  • 使用局部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 提取整个 struct 到局部变量中,修改字段值后再整个赋值
type data struct {
name string
}

func main() {
m := map[string]data{
"x": {"Tom"},
}
r := m["x"]
r.name = "Jerry"
m["x"] = r
fmt.Println(m) // map[x:{Jerry}]
}
  • 使用指向元素的 map 指针
1
2
3
4
5
6
7
8
func main() {
m := map[string]*data{
"x": {"Tom"},
}

m["x"].name = "Jerry" // 直接修改 m["x"] 中的字段
fmt.Println(m["x"]) // &{Jerry}
}

但是要注意下边这种误用:

1
2
3
4
5
6
7
func main() {
m := map[string]*data{
"x": {"Tom"},
}
m["z"].name = "what???"
fmt.Println(m["x"])
}

panic: runtime error: invalid memory address or nil pointer dereference

53. nil interface 和 nil interface 值

虽然 interface 看起来像指针类型,但它不是。interface 类型的变量只有在类型和值均为 nil 时才为 nil

如果你的 interface 变量的值是跟随其他变量变化的(雾),与 nil 比较相等时小心:

1
2
3
4
5
6
7
8
9
10
func main() {
var data *byte
var in interface{}

fmt.Println(data, data == nil) // <nil> true
fmt.Println(in, in == nil) // <nil> true

in = data
fmt.Println(in, in == nil) // <nil> false // data 值为 nil,但 in 值不为 nil
}

如果你的函数返回值类型是 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
// 错误示例
func main() {
doIt := func(arg int) interface{} {
var result *struct{} = nil
if arg > 0 {
result = &struct{}{}
}
return result
}

if res := doIt(-1); res != nil {
fmt.Println("Good result: ", res) // Good result: <nil>
fmt.Printf("%T\n", res) // *struct {} // res 不是 nil,它的值为 nil
fmt.Printf("%v\n", res) // <nil>
}
}

// 正确示例
func main() {
doIt := func(arg int) interface{} {
var result *struct{} = nil
if arg > 0 {
result = &struct{}{}
} else {
return nil // 明确指明返回 nil
}
return result
}

if res := doIt(-1); res != nil {
fmt.Println("Good result: ", res)
} else {
fmt.Println("Bad result: ", res) // Bad result: <nil>
}
}

54. 堆栈变量

你并不总是清楚你的变量是分配到了堆还是栈。

在 C++ 中使用 new 创建的变量总是分配到堆内存上的,但在 Go 中即使使用 new()make() 来创建变量,变量为内存分配位置依旧归 Go 编译器管。

Go 编译器会根据变量的大小及其 “escape analysis” 的结果来决定变量的存储位置,故能准确返回本地变量的地址,这在 C/C++ 中是不行的。

在 go build 或 go run 时,加入 -m 参数,能准确分析程序的变量分配位置:

img

55. GOMAXPROCS、Concurrency(并发)and Parallelism(并行)

Go 1.4 及以下版本,程序只会使用 1 个执行上下文 / OS 线程,即任何时间都最多只有 1 个 goroutine 在执行。

Go 1.5 版本将可执行上下文的数量设置为 runtime.NumCPU() 返回的逻辑 CPU 核心数,这个数与系统实际总的 CPU 逻辑核心数是否一致,取决于你的 CPU 分配给程序的核心数,可以使用 GOMAXPROCS 环境变量或者动态的使用 runtime.GOMAXPROCS() 来调整。

误区:GOMAXPROCS 表示执行 goroutine 的 CPU 核心数,参考文档

GOMAXPROCS 的值是可以超过 CPU 的实际数量的,在 1.5 中最大为 256

1
2
3
4
5
6
7
8
func main() {
fmt.Println(runtime.GOMAXPROCS(-1)) // 4
fmt.Println(runtime.NumCPU()) // 4
runtime.GOMAXPROCS(20)
fmt.Println(runtime.GOMAXPROCS(-1)) // 20
runtime.GOMAXPROCS(300)
fmt.Println(runtime.GOMAXPROCS(-1)) // Go 1.9.2 // 300
}

56. 读写操作的重新排序

Go 可能会重排一些操作的执行顺序,可以保证在一个 goroutine 中操作是顺序执行的,但不保证多 goroutine 的执行顺序:

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
var _ = runtime.GOMAXPROCS(3)

var a, b int

func u1() {
a = 1
b = 2
}

func u2() {
a = 3
b = 4
}

func p() {
println(a)
println(b)
}

func main() {
go u1() // 多个 goroutine 的执行顺序不定
go u2()
go p()
time.Sleep(1 * time.Second)
}

运行效果:

img

如果你想保持多 goroutine 像代码中的那样顺序执行,可以使用 channel 或 sync 包中的锁机制等。

57. 优先调度

你的程序可能出现一个 goroutine 在运行时阻止了其他 goroutine 的运行,比如程序中有一个不让调度器运行的 for 循环:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
done := false

go func() {
done = true
}()

for !done {
}

println("done !")
}

for 的循环体不必为空,但如果代码不会触发调度器执行,将出现问题。

调度器会在 GC、Go 声明、阻塞 channel、阻塞系统调用和锁操作后再执行,也会在非内联函数调用时执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
done := false

go func() {
done = true
}()

for !done {
println("not done !") // 并不内联执行
}

println("done !")
}

可以添加 -m 参数来分析 for 代码块中调用的内联函数:

img

你也可以使用 runtime 包中的 Gosched() 来 手动启动调度器:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
done := false

go func() {
done = true
}()

for !done {
runtime.Gosched()
}

println("done !")
}

运行效果:

img

总结

感谢原作者 kcqon 总结的这篇博客,让我受益匪浅。

由于译者水平有限,不免出现理解失误,望读者在下评论区指出,不胜感激。

Golang精编100题

能力模型

级别 模型
初级
primary 熟悉基本语法,能够看懂代码的意图;
在他人指导下能够完成用户故事的开发,编写的代码符合CleanCode规范;
中级
intermediate 能够独立完成用户故事的开发和测试;
能够嗅出代码的坏味道,并知道如何重构达成目标;
高级
senior 能够开发出高质量高性能的代码;
能够熟练使用高级特性,开发编程框架或测试框架;

选择题

1、**[primary]** 下面属于关键字的是()

A. func

B. def

C. struct

D. class

参考答案:AC

2、**[primary]** 定义一个包内全局字符串变量,下面语法正确的是 ()

A. var str string

B. str := “”

C. str = “”

D. var str = “”

参考答案:AD

3、**[primary]** 通过指针变量 p 访问其成员变量 name,下面语法正确的是()

A. p.name

B. (*p).name

C. (&p).name

D. p->name

参考答案:AB

4、**[primary]** 关于接口和类的说法,下面说法正确的是()

A. 一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口

B. 实现类的时候,只需要关心自己应该提供哪些方法,不用再纠结接口需要拆得多细才合理

C. 类实现接口时,需要导入接口所在的包

D. 接口由使用方按自身需求来定义,使用方无需关心是否有其他模块定义过类似的接口

参考答案:ABD

5、**[primary]** 关于字符串连接,下面语法正确的是()

A. str := ‘abc’ + ‘123’

B. str := “abc” + “123”

C. str := ‘123’ + “abc”

D. fmt.Sprintf(“abc%d”, 123)

参考答案:BD

6、**[primary]** 关于协程,下面说法正确是()

A. 协程和线程都可以实现程序的并发执行

B. 线程比协程更轻量级

C. 协程不存在死锁问题

D. 通过channel来进行协程间的通信

参考答案:AD

7、**[intermediate]** 关于init函数,下面说法正确的是()

A. 一个包中,可以包含多个init函数

B. 程序编译时,先执行导入包的init函数,再执行本包内的init函数

C. main包中,不能有init函数

D. init函数可以被其他函数调用

参考答案:AB

8、**[primary]** 关于循环语句,下面说法正确的有()

A. 循环语句既支持for关键字,也支持while和do-while

B. 关键字for的基本使用方法与C/C++中没有任何差异

C. for循环支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断哪一个循环

D. for循环不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量

参考答案:CD

9、**[intermediate]** 对于函数定义:

1
2
3
4
5
6
7
func add(args ...int) int {
sum := 0
for _, arg := range args {
sum += arg
}
return sum
}
1
2
3
4
5
下面对add函数调用正确的是()
A. add(1, 2)
B. add(1, 3, 7)
C. add([]int{1, 2})
D. add([]int{1, 3, 7}...)

参考答案:ABD

10、**[primary]** 关于类型转化,下面语法正确的是()

A.

1
2
3
type MyInt int
var i int = 1
var j MyInt = i

B.

1
2
3
type MyInt int
var i int = 1
var j MyInt = (MyInt)i

C.

1
2
3
type MyInt int
var i int = 1
var j MyInt = MyInt(i)

D.

1
2
3
type MyInt int
var i int = 1
var j MyInt = i.(MyInt)

参考答案:C

11、**[primary]** 关于局部变量的初始化,下面正确的使用方式是()

A. var i int = 10

B. var i = 10

C. i := 10

D. i = 10

参考答案:ABC

12、**[primary]** 关于const常量定义,下面正确的使用方式是()

A.

1
2
const Pi float64 = 3.14159265358979323846
const zero = 0.0

B.

1
2
3
4
const (
size int64 = 1024
eof = -1
)

C.

1
2
3
4
const (
ERR_ELEM_EXIST error = errors.New("element already exists")
ERR_ELEM_NT_EXIST error = errors.New("element not exists")
)

D.

1
2
const u, v float32 = 0, 3
const a, b, c = 3, 4, "foo"

参考答案:ABD

13、**[primary]** 关于布尔变量b的赋值,下面错误的用法是()

A. b = true

B. b = 1

C. b = bool(1)

D. b = (1 == 2)

参考答案:BC

14、**[intermediate]** 下面的程序的运行结果是()

1
2
3
4
5
6
7
8
func main() {
if (true) {
defer fmt.Printf("1")
} else {
defer fmt.Printf("2")
}
fmt.Printf("3")
}

A. 321

B. 32

C. 31

D. 13

参考答案:C

15、**[primary]** 关于switch语句,下面说法正确的有()

A. 条件表达式必须为常量或者整数

B. 单个case中,可以出现多个结果选项

C. 需要用break来明确退出一个case

D. 只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case

参考答案:BD

16、**[intermediate]** golang中没有隐藏的this指针,这句话的含义是()

A. 方法施加的对象显式传递,没有被隐藏起来

B. golang沿袭了传统面向对象编程中的诸多概念,比如继承、虚函数和构造函数

C. golang的面向对象表达更直观,对于面向过程只是换了一种语法形式来表达

D. 方法施加的对象不需要非得是指针,也不用非得叫this

参考答案:ACD

17、**[intermediate]** golang中的引用类型包括()

A. 数组切片

B. map

C. channel

D. interface

参考答案:ABCD

18、**[intermediate]** golang中的指针运算包括()

A. 可以对指针进行自增或自减运算

B. 可以通过“&”取指针的地址

C. 可以通过“*”取指针指向的数据

D. 可以对指针进行下标运算

参考答案:BC

19、**[primary]** 关于main函数(可执行程序的执行起点),下面说法正确的是()

A. main函数不能带参数

B. main函数不能定义返回值

C. main函数所在的包必须为main包

D. main函数中可以使用flag包来获取和解析命令行参数

参考答案:ABCD

20、**[intermediate]** 下面赋值正确的是()

A. var x = nil

B. var x interface{} = nil

C. var x string = nil

D. var x error = nil

参考答案:BD

21、**[intermediate]** 关于整型切片的初始化,下面正确的是()

A. s := make([]int)

B. s := make([]int, 0)

C. s := make([]int, 5, 10)

D. s := []int{1, 2, 3, 4, 5}

参考答案:BCD

22、**[intermediate]** 从切片中删除一个元素,下面的算法实现正确的是()

A.

1
2
3
4
5
6
7
8
9
10
11
12
13
func (s *Slice)Remove(value interface{}) error {
for i, v := range *s {
if isEqual(value, v) {
if i== len(*s) - 1 {
*s = (*s)[:i]
}else {
*s = append((*s)[:i],(*s)[i + 2:]...)
}
return nil
}
}
return ERR_ELEM_NT_EXIST
}

B.

1
2
3
4
5
6
7
8
9
func (s *Slice)Remove(value interface{}) error {
for i, v := range *s {
if isEqual(value, v) {
*s = append((*s)[:i],(*s)[i + 1:])
return nil
}
}
return ERR_ELEM_NT_EXIST
}

C.

1
2
3
4
5
6
7
8
9
func (s *Slice)Remove(value interface{}) error {
for i, v := range *s {
if isEqual(value, v) {
delete(*s, v)
return nil
}
}
return ERR_ELEM_NT_EXIST
}

D.

1
2
3
4
5
6
7
8
9
func (s *Slice)Remove(value interface{}) error {
for i, v := range *s {
if isEqual(value, v) {
*s = append((*s)[:i],(*s)[i + 1:]...)
return nil
}
}
return ERR_ELEM_NT_EXIST
}

参考答案:D

23、**[primary]** 对于局部变量整型切片x的赋值,下面定义正确的是()

A.

1
2
3
4
x := []int{
1, 2, 3,
4, 5, 6,
}

B.

1
2
3
4
x := []int{
1, 2, 3,
4, 5, 6
}

C.

1
2
3
x := []int{
1, 2, 3,
4, 5, 6}

D.

1
x := []int{1, 2, 3, 4, 5, 6,}

参考答案:ACD

24、**[primary]** 关于变量的自增和自减操作,下面语句正确的是()

A.

1
2
i := 1
i++

B.

1
2
i := 1
j = i++

C.

1
2
i := 1
++i

D.

1
2
i := 1
i--

参考答案:AD

25、**[intermediate]** 关于函数声明,下面语法错误的是()

A. func f(a, b int) (value int, err error)

B. func f(a int, b int) (value int, err error)

C. func f(a, b int) (value int, error)

D. func f(a int, b int) (int, int, error)

参考答案:C

26、**[intermediate]** 如果Add函数的调用代码为:

1
2
3
4
5
6
7
func main() {
var a Integer = 1
var b Integer = 2
var i interface{} = &a
sum := i.(*Integer).Add(b)
fmt.Println(sum)
}

27、则Add函数定义正确的是()

A.

1
2
3
4
type Integer int
func (a Integer) Add(b Integer) Integer {
return a + b
}

B.

1
2
3
4
type Integer int
func (a Integer) Add(b *Integer) Integer {
return a + *b
}

C.

1
2
3
4
type Integer int
func (a *Integer) Add(b Integer) Integer {
return *a + b
}

D.

1
2
3
4
type Integer int
func (a *Integer) Add(b *Integer) Integer {
return *a + *b
}

参考答案:AC

28、**[intermediate]** 如果Add函数的调用代码为:

1
2
3
4
5
6
7
func main() {
var a Integer = 1
var b Integer = 2
var i interface{} = a
sum := i.(Integer).Add(b)
fmt.Println(sum)
}

29、则Add函数定义正确的是()

A.

1
2
3
4
type Integer int
func (a Integer) Add(b Integer) Integer {
return a + b
}

B.

1
2
3
4
type Integer int
func (a Integer) Add(b *Integer) Integer {
return a + *b
}

C.

1
2
3
4
type Integer int
func (a *Integer) Add(b Integer) Integer {
return *a + b
}

D.

1
2
3
4
type Integer int
func (a *Integer) Add(b *Integer) Integer {
return *a + *b
}

参考答案:A

30、**[intermediate]** 关于GetPodAction定义,下面赋值正确的是()

1
2
3
4
5
6
7
8
9
type Fragment interface {
Exec(transInfo *TransInfo) error
}
type GetPodAction struct {
}
func (g GetPodAction) Exec(transInfo *TransInfo) error {
...
return nil
}

A. var fragment Fragment = new(GetPodAction)

B. var fragment Fragment = GetPodAction

C. var fragment Fragment = &GetPodAction{}

D. var fragment Fragment = GetPodAction{}

参考答案:ACD

31、**[intermediate]** 关于GoMock,下面说法正确的是()

A. GoMock可以对interface打桩

B. GoMock可以对类的成员函数打桩

C. GoMock可以对函数打桩

D. GoMock打桩后的依赖注入可以通过GoStub完成

参考答案:AD

32、**[intermediate]** 关于接口,下面说法正确的是()

A. 只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就是等价的,可以相互赋值

B. 如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A

C. 接口查询是否成功,要在运行期才能够确定

D. 接口赋值是否可行,要在运行期才能够确定

参考答案:ABC

33、**[primary]** 关于channel,下面语法正确的是()

A. var ch chan int

B. ch := make(chan int)

C. <- ch

D. ch <-

参考答案:ABC

34、**[primary]** 关于同步锁,下面说法正确的是()

A. 当一个goroutine获得了Mutex后,其他goroutine就只能乖乖的等待,除非该goroutine释放这个Mutex

B. RWMutex在读锁占用的情况下,会阻止写,但不阻止读

C. RWMutex在写锁占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占

D. Lock()操作需要保证有Unlock()或RUnlock()调用与之对应

参考答案:ABC

35、**[intermediate]** golang中大多数数据类型都可以转化为有效的JSON文本,下面几种类型除外()

A. 指针

B. channel

C. complex

D. 函数

参考答案:BCD

36、**[intermediate]** 关于go vendor,下面说法正确的是()

A. 基本思路是将引用的外部包的源代码放在当前工程的vendor目录下面

B. 编译go代码会优先从vendor目录先寻找依赖包

C. 可以指定引用某个特定版本的外部包

D. 有了vendor目录后,打包当前的工程代码到其他机器的$GOPATH/src下都可以通过编译

参考答案:ABD

37、**[primary]** flag是bool型变量,下面if表达式符合编码规范的是()

A. if flag == 1

B. if flag

C. if flag == false

D. if !flag

参考答案:BD

38、**[primary]** value是整型变量,下面if表达式符合编码规范的是()

A. if value == 0

B. if value

C. if value != 0

D. if !value

参考答案:AC

39、**[intermediate]** 关于函数返回值的错误设计,下面说法正确的是()

A. 如果失败原因只有一个,则返回bool

B. 如果失败原因超过一个,则返回error

C. 如果没有失败原因,则不返回bool或error

D. 如果重试几次可以避免失败,则不要立即返回bool或error

参考答案:ABCD

40、**[intermediate]** 关于异常设计,下面说法正确的是()

A. 在程序开发阶段,坚持速错,让程序异常崩溃

B. 在程序部署后,应恢复异常避免程序终止

C. 一切皆错误,不用进行异常设计

D. 对于不应该出现的分支,使用异常处理

参考答案:ABD

41、**[intermediate]** 关于slice或map操作,下面正确的是()

A.

1
2
var s []int
s = append(s,1)

B.

1
2
var m map[string]int
m["one"] = 1

C.

1
2
3
var s []int
s = make([]int, 0)
s = append(s,1)

D.

1
2
3
var m map[string]int
m = make(map[string]int)
m["one"] = 1

参考答案:ACD

42、**[intermediate]** 关于channel的特性,下面说法正确的是()

A. 给一个 nil channel 发送数据,造成永远阻塞

B. 从一个 nil channel 接收数据,造成永远阻塞

C. 给一个已经关闭的 channel 发送数据,引起 panic

D. 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值

参考答案:ABCD

43、**[intermediate]** 关于无缓冲和有冲突的channel,下面说法正确的是()

A. 无缓冲的channel是默认的缓冲为1的channel

B. 无缓冲的channel和有缓冲的channel都是同步的

C. 无缓冲的channel和有缓冲的channel都是非同步的

D. 无缓冲的channel是同步的,而有缓冲的channel是非同步的

参考答案:D

44、**[intermediate]** 关于异常的触发,下面说法正确的是()

A. 空指针解析

B. 下标越界

C. 除数为0

D. 调用panic函数

参考答案:ABCD

45、**[intermediate]** 关于cap函数的适用类型,下面说法正确的是()

A. array

B. slice

C. map

D. channel

参考答案:ABD

46、**[intermediate]** 关于beego框架,下面说法正确的是()

A. beego是一个golang实现的轻量级HTTP框架

B. beego可以通过注释路由、正则路由等多种方式完成url路由注入

C. 可以使用bee new工具生成空工程,然后使用bee run命令自动热编译

D. beego框架只提供了对url路由的处理, 而对于MVC架构中的数据库部分未提供框架支持

参考答案:ABC

47、**[intermediate]** 关于goconvey,下面说法正确的是()

A. goconvey是一个支持golang的单元测试框架

B. goconvey能够自动监控文件修改并启动测试,并可以将测试结果实时输出到web界面

C. goconvey提供了丰富的断言简化测试用例的编写

D. goconvey无法与go test集成

参考答案:ABC

48、**[intermediate]** 关于go vet,下面说法正确的是()

A. go vet是golang自带工具go tool vet的封装

B. 当执行go vet database时,可以对database所在目录下的所有子文件夹进行递归检测

C. go vet可以使用绝对路径、相对路径或相对GOPATH的路径指定待检测的包

D. go vet可以检测出死代码

参考答案:ACD

49、**[intermediate]** 关于map,下面说法正确的是()

A. map反序列化时json.unmarshal的入参必须为map的地址

B. 在函数调用中传递map,则子函数中对map元素的增加不会导致父函数中map的修改

C. 在函数调用中传递map,则子函数中对map元素的修改不会导致父函数中map的修改

D. 不能使用内置函数delete删除map的元素

参考答案:A

50、**[intermediate]** 关于GoStub,下面说法正确的是()

A. GoStub可以对全局变量打桩

B. GoStub可以对函数打桩

C. GoStub可以对类的成员方法打桩

D. GoStub可以打动态桩,比如对一个函数打桩后,多次调用该函数会有不同的行为

参考答案:ABD

51、**[primary]** 关于select机制,下面说法正确的是()

A. select机制用来处理异步IO问题

B. select机制最大的一条限制就是每个case语句里必须是一个IO操作

C. golang在语言级别支持select关键字

D. select关键字的用法与switch语句非常类似,后面要带判断条件

参考答案:ABC

52、**[primary]** 关于内存泄露,下面说法正确的是()

A. golang有自动垃圾回收,不存在内存泄露

B. golang中检测内存泄露主要依靠的是pprof包

C. 内存泄露可以在编译阶段发现

D. 应定期使用浏览器来查看系统的实时内存信息,及时发现内存泄露问题

参考答案:BD

填空题

1、**[primary]** 声明一个整型变量i__________

参考答案:var i int

2、**[primary]** 声明一个含有10个元素的整型数组a__________

参考答案:var a [10]int

3、**[primary]** 声明一个整型数组切片s__________

参考答案:var s []int

4、**[primary]** 声明一个整型指针变量p__________

参考答案:var p *int

5、**[primary]** 声明一个key为字符串型value为整型的map变量m__________

参考答案:var m map[string]int

6、**[primary]** 声明一个入参和返回值均为整型的函数变量f__________

参考答案:var f func(a int) int

7、**[primary]** 声明一个只用于读取int数据的单向channel变量ch__________

参考答案:var ch <-chan int

8、**[primary]** 假设源文件的命名为slice.go,则测试文件的命名为__________

参考答案:slice_test.go

9、**[primary]** go test要求测试函数的前缀必须命名为__________

参考答案:Test

10、**[intermediate]** 下面的程序的运行结果是__________

1
2
3
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}

参考答案:4 3 2 1 0

11、**[intermediate]** 下面的程序的运行结果是__________

1
2
3
4
5
6
7
8
func main() {
x := 1
{
x := 2
fmt.Print(x)
}
fmt.Println(x)
}

参考答案:21

12、**[intermediate]** 下面的程序的运行结果是__________

1
2
3
4
5
6
7
8
9
10
11
func main() {
strs := []string{"one", "two", "three"}

for _, s := range strs {
go func() {
time.Sleep(1 * time.Second)
fmt.Printf("%s ", s)
}()
}
time.Sleep(3 * time.Second)
}

参考答案:three three three

13、**[intermediate]** 下面的程序的运行结果是__________

1
2
3
4
5
6
func main() {
x := []string{"a", "b", "c"}
for v := range x {
fmt.Print(v)
}
}

参考答案:012

14、**[intermediate]** 下面的程序的运行结果是__________

1
2
3
4
5
6
func main() {
x := []string{"a", "b", "c"}
for _, v := range x {
fmt.Print(v)
}
}

参考答案:abc

15、**[primary]** 下面的程序的运行结果是__________

1
2
3
4
5
6
func main() {
i := 1
j := 2
i, j = j, i
fmt.Printf("%d%d\n", i, j)
}

参考答案:21

16、**[primary]** 下面的程序的运行结果是__________

1
2
3
4
5
6
7
8
9
func incr(p *int) int {
*p++
return *p
}
func main() {
v := 1
incr(&v)
fmt.Println(v)
}

参考答案:2

17、**[primary]** 启动一个goroutine的关键字是__________

参考答案:go

18、**[intermediate]** 下面的程序的运行结果是__________

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Slice []int
func NewSlice() Slice {
return make(Slice, 0)
}
func (s* Slice) Add(elem int) *Slice {
*s = append(*s, elem)
fmt.Print(elem)
return s
}
func main() {
s := NewSlice()
defer s.Add(1).Add(2)
s.Add(3)
}

参考答案:132

判断题

1、**[primary]** 数组是一个值类型()

参考答案:T

2、**[primary]** 使用map不需要引入任何库()

参考答案:T

3、**[intermediate]** 内置函数delete可以删除数组切片内的元素()

参考答案:F

4、**[primary]** 指针是基础类型()

参考答案:F

5、**[primary]** interface{}是可以指向任意对象的Any类型()

参考答案:T

6、**[intermediate]** 下面关于文件操作的代码可能触发异常()

1
2
3
4
5
6
7
file, err := os.Open("test.go")
defer file.Close()
if err != nil {
fmt.Println("open file failed:", err)
return
}
...

参考答案:T

7、**[primary]** Golang不支持自动垃圾回收()

参考答案:F

8、**[primary]** Golang支持反射,反射最常见的使用场景是做对象的序列化()

参考答案:T

9、**[primary]** Golang可以复用C/C++的模块,这个功能叫Cgo()

参考答案:F

10、**[primary]** 下面代码中两个斜点之间的代码,比如json:"x",作用是X字段在从结构体实例编码到JSON数据格式的时候,使用x作为名字,这可以看作是一种重命名的方式()

1
2
3
4
5
type Position struct {
X int `json:"x"`
Y int `json:"y"`
Z int `json:"z"`
}

参考答案:T

11、**[primary]** 通过成员变量或函数首字母的大小写来决定其作用域()

参考答案:T

12、**[primary]** 对于常量定义zero(const zero = 0.0),zero是浮点型常量()

参考答案:F

13、**[primary]** 对变量x的取反操作是~x()

参考答案:F

14、**[primary]** 下面的程序的运行结果是xello()

1
2
3
4
5
func main() {
str := "hello"
str[0] = 'x'
fmt.Println(str)
}

参考答案:F

15、**[primary]** golang支持goto语句()

参考答案:T

16、**[primary]** 下面代码中的指针p为野指针,因为返回的栈内存在函数结束时会被释放()

1
2
3
4
5
6
7
8
9
10
type TimesMatcher struct {
base int
}
func NewTimesMatcher(base int) *TimesMatcher{
return &TimesMatcher{base:base}
}
func main() {
p := NewTimesMatcher(3)
...
}

参考答案:F

17、**[primary]** 匿名函数可以直接赋值给一个变量或者直接执行()

参考答案:T

18、**[primary]** 如果调用方调用了一个具有多返回值的方法,但是却不想关心其中的某个返回值,可以简单地用一个下划线“_”来跳过这个返回值,该下划线对应的变量叫匿名变量()

参考答案:T

19、**[primary]** 在函数的多返回值中,如果有error或bool类型,则一般放在最后一个()

参考答案:T

20、**[primary]** 错误是业务过程的一部分,而异常不是()

参考答案:T

21、**[primary]** 函数执行时,如果由于panic导致了异常,则延迟函数不会执行()

参考答案:F

22、**[intermediate]** 当程序运行时,如果遇到引用空指针、下标越界或显式调用panic函数等情况,则先触发panic函数的执行,然后调用延迟函数。调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数。如果一路在延迟函数中没有recover函数的调用,则会到达该携程的起点,该携程结束,然后终止其他所有携程,其他携程的终止过程也是重复发生:函数停止执行,调用延迟执行函数()

参考答案:F

23、**[primary]** 同级文件的包名不允许有多个()

参考答案:T

24、**[intermediate]** 可以给任意类型添加相应的方法()

参考答案:F

25、**[primary]** golang虽然没有显式的提供继承语法,但是通过匿名组合实现了继承()

参考答案:T

26、**[primary]** 使用for range迭代map时每次迭代的顺序可能不一样,因为map的迭代是随机的()

参考答案:T

27、**[primary]** switch后面可以不跟表达式()

参考答案:T

28、**[intermediate]** 结构体在序列化时非导出变量(以小写字母开头的变量名)不会被encode,因此在decode时这些非导出变量的值为其类型的零值()

参考答案:T

29、**[primary]** golang中没有构造函数的概念,对象的创建通常交由一个全局的创建函数来完成,以NewXXX来命名()

参考答案:T

30、**[intermediate]** 当函数deferDemo返回失败时,并不能destroy已create成功的资源()

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
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
defer func() {
if err != nil {
destroyResource1()
}
}()

err = createResource2()
if err != nil {
return ERR_CREATE_RESOURCE2_FAILED
}
defer func() {
if err != nil {
destroyResource2()
}
}()

err = createResource3()
if err != nil {
return ERR_CREATE_RESOURCE3_FAILED
}
return nil
}

参考答案:F

31、**[intermediate]** channel本身必然是同时支持读写的,所以不存在单向channel()

参考答案:F

32、**[primary]** import后面的最后一个元素是包名()

参考答案:F

UberGo语言编码规范中文版

forked from uber_go_guide_cn

uber-go/guide 的中文翻译

English

Uber Go 语言编码规范

Uber 是一家美国硅谷的科技公司,也是 Go 语言的早期 adopter。其开源了很多 golang 项目,诸如被 Gopher 圈熟知的 zapjaeger 等。2018 年年末 Uber 将内部的 Go 风格规范 开源到 GitHub,经过一年的积累和更新,该规范已经初具规模,并受到广大 Gopher 的关注。本文是该规范的中文版本。本版本会根据原版实时更新。

版本

  • 当前更新版本:2021-07-09 版本地址:commit:#130

目录

介绍

样式 (style) 是支配我们代码的惯例。术语样式有点用词不当,因为这些约定涵盖的范围不限于由 gofmt 替我们处理的源文件格式。

本指南的目的是通过详细描述在 Uber 编写 Go 代码的注意事项来管理这种复杂性。这些规则的存在是为了使代码库易于管理,同时仍然允许工程师更有效地使用 Go 语言功能。

该指南最初由 Prashant VaranasiSimon Newton 编写,目的是使一些同事能快速使用 Go。多年来,该指南已根据其他人的反馈进行了修改。

本文档记录了我们在 Uber 遵循的 Go 代码中的惯用约定。其中许多是 Go 的通用准则,而其他扩展准则依赖于下面外部的指南:

  1. Effective Go
  2. Go Common Mistakes
  3. Go Code Review Comments

所有代码都应该通过golintgo vet的检查并无错误。我们建议您将编辑器设置为:

  • 保存时运行 goimports
  • 运行 golintgo vet 检查错误

您可以在以下 Go 编辑器工具支持页面中找到更为详细的信息:
https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

指导原则

指向 interface 的指针

您几乎不需要指向接口类型的指针。您应该将接口作为值进行传递,在这样的传递过程中,实质上传递的底层数据仍然可以是指针。

接口实质上在底层用两个字段表示:

  1. 一个指向某些特定类型信息的指针。您可以将其视为”type”。
  2. 数据指针。如果存储的数据是指针,则直接存储。如果存储的数据是一个值,则存储指向该值的指针。

如果希望接口方法修改基础数据,则必须使用指针传递(将对象指针赋值给接口变量)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type F interface {
f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

// f1.f()无法修改底层数据
// f2.f() 可以修改底层数据,给接口变量f2赋值时使用的是对象指针
var f1 F = S1{}
var f2 F = &S2{}

Interface 合理性验证

在编译时验证接口的符合性。这包括:

  • 将实现特定接口的导出类型作为接口API 的一部分进行检查
  • 实现同一接口的(导出和非导出)类型属于实现类型的集合
  • 任何违反接口合理性检查的场景,都会终止编译,并通知给用户

补充:上面3条是编译器对接口的检查机制,
大体意思是错误使用接口会在编译期报错.
所以可以利用这个机制让部分问题在编译期暴露.

BadGood
1
2
3
4
5
6
7
8
9
10
// 如果Handler没有实现http.Handler,会在运行时报错
type Handler struct {
// ...
}
func (h *Handler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
...
}
1
2
3
4
5
6
7
8
9
10
11
12
type Handler struct {
// ...
}
// 用于触发编译期的接口的合理性检查机制
// 如果Handler没有实现http.Handler,会在编译期报错
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}

如果 *Handlerhttp.Handler 的接口不匹配,
那么语句 var _ http.Handler = (*Handler)(nil) 将无法编译通过.

赋值的右边应该是断言类型的零值。
对于指针类型(如 *Handler)、切片和映射,这是 nil
对于结构类型,这是空结构。

1
2
3
4
5
6
7
8
9
10
11
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}

接收器 (receiver) 与接口

使用值接收器的方法既可以通过值调用,也可以通过指针调用。

带指针接收器的方法只能通过指针或 addressable values调用.

例如,

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
type S struct {
data string
}

func (s S) Read() string {
return s.data
}

func (s *S) Write(str string) {
s.data = str
}

sVals := map[int]S{1: {"A"}}

// 你只能通过值调用 Read
sVals[1].Read()

// 这不能编译通过:
// sVals[1].Write("test")

sPtrs := map[int]*S{1: {"A"}}

// 通过指针既可以调用 Read,也可以调用 Write 方法
sPtrs[1].Read()
sPtrs[1].Write("test")

类似的,即使方法有了值接收器,也同样可以用指针接收器来满足接口.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type F interface {
f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}

var i F
i = s1Val
i = s1Ptr
i = s2Ptr

// 下面代码无法通过编译。因为 s2Val 是一个值,而 S2 的 f 方法中没有使用值接收器
// i = s2Val

Effective Go 中有一段关于 pointers vs. values 的精彩讲解。

补充:

  • 一个类型可以有值接收器方法集和指针接收器方法集
    • 值接收器方法集是指针接收器方法集的子集,反之不是
  • 规则
    • 值对象只可以使用值接收器方法集
    • 指针对象可以使用 值接收器方法集 + 指针接收器方法集
  • 接口的匹配(或者叫实现)
    • 类型实现了接口的所有方法,叫匹配
    • 具体的讲,要么是类型的值方法集匹配接口,要么是指针方法集匹配接口

具体的匹配分两种:

  • 值方法集和接口匹配
    • 给接口变量赋值的不管是值还是指针对象,都ok,因为都包含值方法集
  • 指针方法集和接口匹配
    • 只能将指针对象赋值给接口变量,因为只有指针方法集和接口匹配
    • 如果将值对象赋值给接口变量,会在编译期报错(会触发接口合理性检查机制)

为啥 i = s2Val 会报错,因为值方法集和接口不匹配.

零值 Mutex 是有效的

零值 sync.Mutexsync.RWMutex 是有效的。所以指向 mutex 的指针基本是不必要的。

BadGood
1
2
mu := new(sync.Mutex)
mu.Lock()
1
2
var mu sync.Mutex
mu.Lock()

如果你使用结构体指针,mutex 应该作为结构体的非指针字段。即使该结构体不被导出,也不要直接把 mutex 嵌入到结构体中。

BadGood
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type SMap struct {
sync.Mutex

data map[string]string
}

func NewSMap() *SMap {
return &SMap{
data: make(map[string]string),
}
}

func (m *SMap) Get(k string) string {
m.Lock()
defer m.Unlock()

return m.data[k]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type SMap struct {
mu sync.Mutex

data map[string]string
}

func NewSMap() *SMap {
return &SMap{
data: make(map[string]string),
}
}

func (m *SMap) Get(k string) string {
m.mu.Lock()
defer m.mu.Unlock()

return m.data[k]
}

Mutex 字段, LockUnlock 方法是 SMap 导出的 API 中不刻意说明的一部分。

mutex 及其方法是 SMap 的实现细节,对其调用者不可见。

在边界处拷贝 Slices 和 Maps

slices 和 maps 包含了指向底层数据的指针,因此在需要复制它们时要特别注意。

接收 Slices 和 Maps

请记住,当 map 或 slice 作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改。

Bad Good
1
2
3
4
5
6
7
8
9
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}

trips := ...
d1.SetTrips(trips)

// 你是要修改 d1.trips 吗?
trips[0] = ...
1
2
3
4
5
6
7
8
9
10
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}

trips := ...
d1.SetTrips(trips)

// 这里我们修改 trips[0],但不会影响到 d1.trips
trips[0] = ...

返回 slices 或 maps

同样,请注意用户对暴露内部状态的 map 或 slice 的修改。

BadGood
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Stats struct {
mu sync.Mutex

counters map[string]int
}

// Snapshot 返回当前状态。
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()

return s.counters
}

// snapshot 不再受互斥锁保护
// 因此对 snapshot 的任何访问都将受到数据竞争的影响
// 影响 stats.counters
snapshot := stats.Snapshot()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Stats struct {
mu sync.Mutex

counters map[string]int
}

func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()

result := make(map[string]int, len(s.counters))
for k, v := range s.counters {
result[k] = v
}
return result
}

// snapshot 现在是一个拷贝
snapshot := stats.Snapshot()

使用 defer 释放资源

使用 defer 释放资源,诸如文件和锁。

BadGood
1
2
3
4
5
6
7
8
9
10
11
12
13
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// 当有多个 return 分支时,很容易遗忘 unlock
1
2
3
4
5
6
7
8
9
10
11
p.Lock()
defer p.Unlock()

if p.count < 10 {
return p.count
}

p.count++
return p.count

// 更可读

Defer 的开销非常小,只有在您可以证明函数执行时间处于纳秒级的程度时,才应避免这样做。使用 defer 提升可读性是值得的,因为使用它们的成本微不足道。尤其适用于那些不仅仅是简单内存访问的较大的方法,在这些方法中其他计算的资源消耗远超过 defer

Channel 的 size 要么是 1,要么是无缓冲的

channel 通常 size 应为 1 或是无缓冲的。默认情况下,channel 是无缓冲的,其 size 为零。任何其他尺寸都必须经过严格的审查。我们需要考虑如何确定大小,考虑是什么阻止了 channel 在高负载下和阻塞写时的写入,以及当这种情况发生时系统逻辑有哪些变化。(翻译解释:按照原文意思是需要界定通道边界,竞态条件,以及逻辑上下文梳理)

BadGood
1
2
// 应该足以满足任何情况!
c := make(chan int, 64)
1
2
3
4
// 大小:1
c := make(chan int, 1) // 或者
// 无缓冲 channel,大小为 0
c := make(chan int)

枚举从 1 开始

在 Go 中引入枚举的标准方法是声明一个自定义类型和一个使用了 iota 的 const 组。由于变量的默认值为 0,因此通常应以非零值开头枚举。

BadGood
1
2
3
4
5
6
7
8
9
type Operation int

const (
Add Operation = iota
Subtract
Multiply
)

// Add=0, Subtract=1, Multiply=2
1
2
3
4
5
6
7
8
9
type Operation int

const (
Add Operation = iota + 1
Subtract
Multiply
)

// Add=1, Subtract=2, Multiply=3

在某些情况下,使用零值是有意义的(枚举从零开始),例如,当零值是理想的默认行为时。

1
2
3
4
5
6
7
8
9
type LogOutput int

const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2

使用 time 处理时间

时间处理很复杂。关于时间的错误假设通常包括以下几点。

  1. 一天有 24 小时
  2. 一小时有 60 分钟
  3. 一周有七天
  4. 一年 365 天
  5. 还有更多

例如,1 表示在一个时间点上加上 24 小时并不总是产生一个新的日历日。

因此,在处理时间时始终使用 "time" 包,因为它有助于以更安全、更准确的方式处理这些不正确的假设。

使用 time.Time 表达瞬时时间

在处理时间的瞬间时使用 time.Time,在比较、添加或减去时间时使用 time.Time 中的方法。

BadGood
1
2
3
func isActive(now, start, stop int) bool {
return start <= now && now < stop
}
1
2
3
func isActive(now, start, stop time.Time) bool {
return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}

使用 time.Duration 表达时间段

在处理时间段时使用 time.Duration .

BadGood
1
2
3
4
5
6
7
func poll(delay int) {
for {
// ...
time.Sleep(time.Duration(delay) * time.Millisecond)
}
}
poll(10) // 是几秒钟还是几毫秒?
1
2
3
4
5
6
7
func poll(delay time.Duration) {
for {
// ...
time.Sleep(delay)
}
}
poll(10*time.Second)

回到第一个例子,在一个时间瞬间加上 24 小时,我们用于添加时间的方法取决于意图。如果我们想要下一个日历日(当前天的下一天)的同一个时间点,我们应该使用 Time.AddDate。但是,如果我们想保证某一时刻比前一时刻晚 24 小时,我们应该使用 Time.Add

1
2
newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */)
maybeNewDay := t.Add(24 * time.Hour)

对外部系统使用 time.Timetime.Duration

尽可能在与外部系统的交互中使用 time.Durationtime.Time 例如 :

当不能在这些交互中使用 time.Duration 时,请使用 intfloat64,并在字段名称中包含单位。

例如,由于 encoding/json 不支持 time.Duration,因此该单位包含在字段的名称中。

BadGood
1
2
3
4
// {"interval": 2}
type Config struct {
Interval int `json:"interval"`
}
1
2
3
4
// {"intervalMillis": 2000}
type Config struct {
IntervalMillis int `json:"intervalMillis"`
}

当在这些交互中不能使用 time.Time 时,除非达成一致,否则使用 stringRFC 3339 中定义的格式时间戳。默认情况下,Time.UnmarshalText 使用此格式,并可通过 time.RFC3339Time.Formattime.Parse 中使用。

尽管这在实践中并不成问题,但请记住,"time" 包不支持解析闰秒时间戳(8728),也不在计算中考虑闰秒(15190)。如果您比较两个时间瞬间,则差异将不包括这两个瞬间之间可能发生的闰秒。

错误类型

Go 中有多种声明错误(Error) 的选项:

返回错误时,请考虑以下因素以确定最佳选择:

  • 这是一个不需要额外信息的简单错误吗?如果是这样,errors.New 足够了。

  • 客户需要检测并处理此错误吗?如果是这样,则应使用自定义类型并实现该 Error() 方法。

  • 您是否正在传播下游函数返回的错误?如果是这样,请查看本文后面有关错误包装 section on error wrapping 部分的内容。

  • 否则 fmt.Errorf 就可以了。

如果客户端需要检测错误,并且您已使用创建了一个简单的错误 errors.New,请使用一个错误变量。

BadGood
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// package foo

func Open() error {
return errors.New("could not open")
}

// package bar

func use() {
if err := foo.Open(); err != nil {
if err.Error() == "could not open" {
// handle
} else {
panic("unknown error")
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
return ErrCouldNotOpen
}

// package bar

if err := foo.Open(); err != nil {
if errors.Is(err, foo.ErrCouldNotOpen) {
// handle
} else {
panic("unknown error")
}
}

如果您有可能需要客户端检测的错误,并且想向其中添加更多信息(例如,它不是静态字符串),则应使用自定义类型。

BadGood
1
2
3
4
5
6
7
8
9
10
11
12
13
func open(file string) error {
return fmt.Errorf("file %q not found", file)
}

func use() {
if err := open("testfile.txt"); err != nil {
if strings.Contains(err.Error(), "not found") {
// handle
} else {
panic("unknown error")
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type errNotFound struct {
file string
}

func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
}

func open(file string) error {
return errNotFound{file: file}
}

func use() {
if err := open("testfile.txt"); err != nil {
if _, ok := err.(errNotFound); ok {
// handle
} else {
panic("unknown error")
}
}
}

直接导出自定义错误类型时要小心,因为它们已成为程序包公共 API 的一部分。最好公开匹配器功能以检查错误。

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 foo

type errNotFound struct {
file string
}

func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
}

func IsNotFoundError(err error) bool {
_, ok := err.(errNotFound)
return ok
}

func Open(file string) error {
return errNotFound{file: file}
}

// package bar

if err := foo.Open("foo"); err != nil {
if foo.IsNotFoundError(err) {
// handle
} else {
panic("unknown error")
}
}

错误包装 (Error Wrapping)

一个(函数/方法)调用失败时,有三种主要的错误传播方式:

  • 如果没有要添加的其他上下文,并且您想要维护原始错误类型,则返回原始错误。
  • 添加上下文,使用 "pkg/errors".Wrap 以便错误消息提供更多上下文 ,"pkg/errors".Cause 可用于提取原始错误。
  • 如果调用者不需要检测或处理的特定错误情况,使用 fmt.Errorf

建议在可能的地方添加上下文,以使您获得诸如“调用服务 foo:连接被拒绝”之类的更有用的错误,而不是诸如“连接被拒绝”之类的模糊错误。

在将上下文添加到返回的错误时,请避免使用“failed to”之类的短语以保持上下文简洁,这些短语会陈述明显的内容,并随着错误在堆栈中的渗透而逐渐堆积:

BadGood
1
2
3
4
5
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %v", err)
}
1
2
3
4
5
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %v", err)
}
1
failed to x: failed to y: failed to create new store: the error
1
x: y: new store: the error

但是,一旦将错误发送到另一个系统,就应该明确消息是错误消息(例如使用err标记,或在日志中以”Failed”为前缀)。

另请参见 Don’t just check errors, handle them gracefully. 不要只是检查错误,要优雅地处理错误

处理类型断言失败

type assertion 的单个返回值形式针对不正确的类型将产生 panic。因此,请始终使用“comma ok”的惯用法。

BadGood
1
t := i.(string)
1
2
3
4
t, ok := i.(string)
if !ok {
// 优雅地处理错误
}

不要 panic

在生产环境中运行的代码必须避免出现 panic。panic 是 cascading failures 级联失败的主要根源 。如果发生错误,该函数必须返回错误,并允许调用方决定如何处理它。

BadGood
1
2
3
4
5
6
7
8
9
10
func run(args []string) {
if len(args) == 0 {
panic("an argument is required")
}
// ...
}

func main() {
run(os.Args[1:])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func run(args []string) error {
if len(args) == 0 {
return errors.New("an argument is required")
}
// ...
return nil
}

func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

panic/recover 不是错误处理策略。仅当发生不可恢复的事情(例如:nil 引用)时,程序才必须 panic。程序初始化是一个例外:程序启动时应使程序中止的不良情况可能会引起 panic。

1
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

即使在测试代码中,也优先使用t.Fatal或者t.FailNow而不是 panic 来确保失败被标记。

BadGood
1
2
3
4
5
6
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
panic("failed to set up test")
}
1
2
3
4
5
6
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
t.Fatal("failed to set up test")
}

使用 go.uber.org/atomic

使用 sync/atomic 包的原子操作对原始类型 (int32, int64等)进行操作,因为很容易忘记使用原子操作来读取或修改变量。

go.uber.org/atomic 通过隐藏基础类型为这些操作增加了类型安全性。此外,它包括一个方便的atomic.Bool类型。

BadGood
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type foo struct {
running int32 // atomic
}

func (f* foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
// already running…
return
}
// start the Foo
}

func (f *foo) isRunning() bool {
return f.running == 1 // race!
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type foo struct {
running atomic.Bool
}

func (f *foo) start() {
if f.running.Swap(true) {
// already running…
return
}
// start the Foo
}

func (f *foo) isRunning() bool {
return f.running.Load()
}

避免可变全局变量

使用选择依赖注入方式避免改变全局变量。
既适用于函数指针又适用于其他值类型

BadGood
1
2
3
4
5
6
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// sign.go
type signer struct {
now func() time.Time
}
func newSigner() *signer {
return &signer{
now: time.Now,
}
}
func (s *signer) Sign(msg string) string {
now := s.now()
return signWithTime(msg, now)
}
1
2
3
4
5
6
7
8
9
// sign_test.go
func TestSign(t *testing.T) {
oldTimeNow := _timeNow
_timeNow = func() time.Time {
return someFixedTime
}
defer func() { _timeNow = oldTimeNow }()
assert.Equal(t, want, sign(give))
}
1
2
3
4
5
6
7
8
// sign_test.go
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}
assert.Equal(t, want, s.Sign(give))
}

避免在公共结构中嵌入类型

这些嵌入的类型泄漏实现细节、禁止类型演化和模糊的文档。

假设您使用共享的 AbstractList 实现了多种列表类型,请避免在具体的列表实现中嵌入 AbstractList
相反,只需手动将方法写入具体的列表,该列表将委托给抽象列表。

1
2
3
4
5
6
7
8
9
type AbstractList struct {}
// 添加将实体添加到列表中。
func (l *AbstractList) Add(e Entity) {
// ...
}
// 移除从列表中移除实体。
func (l *AbstractList) Remove(e Entity) {
// ...
}
BadGood
1
2
3
4
// ConcreteList 是一个实体列表。
type ConcreteList struct {
*AbstractList
}
1
2
3
4
5
6
7
8
9
10
11
12
// ConcreteList 是一个实体列表。
type ConcreteList struct {
list *AbstractList
}
// 添加将实体添加到列表中。
func (l *ConcreteList) Add(e Entity) {
l.list.Add(e)
}
// 移除从列表中移除实体。
func (l *ConcreteList) Remove(e Entity) {
l.list.Remove(e)
}

Go 允许 类型嵌入 作为继承和组合之间的折衷。
外部类型获取嵌入类型的方法的隐式副本。
默认情况下,这些方法委托给嵌入实例的同一方法。

结构还获得与类型同名的字段。
所以,如果嵌入的类型是 public,那么字段是 public。为了保持向后兼容性,外部类型的每个未来版本都必须保留嵌入类型。

很少需要嵌入类型。
这是一种方便,可以帮助您避免编写冗长的委托方法。

即使嵌入兼容的抽象列表 interface,而不是结构体,这将为开发人员提供更大的灵活性来改变未来,但仍然泄露了具体列表使用抽象实现的细节。

BadGood
1
2
3
4
5
6
7
8
9
// AbstractList 是各种实体列表的通用实现。
type AbstractList interface {
Add(Entity)
Remove(Entity)
}
// ConcreteList 是一个实体列表。
type ConcreteList struct {
AbstractList
}
1
2
3
4
5
6
7
8
9
10
11
12
// ConcreteList 是一个实体列表。
type ConcreteList struct {
list AbstractList
}
// 添加将实体添加到列表中。
func (l *ConcreteList) Add(e Entity) {
l.list.Add(e)
}
// 移除从列表中移除实体。
func (l *ConcreteList) Remove(e Entity) {
l.list.Remove(e)
}

无论是使用嵌入式结构还是使用嵌入式接口,嵌入式类型都会限制类型的演化.

  • 向嵌入式接口添加方法是一个破坏性的改变。
  • 删除嵌入类型是一个破坏性的改变。
  • 即使使用满足相同接口的替代方法替换嵌入类型,也是一个破坏性的改变。

尽管编写这些委托方法是乏味的,但是额外的工作隐藏了实现细节,留下了更多的更改机会,还消除了在文档中发现完整列表接口的间接性操作。

避免使用内置名称

Go语言规范language specification 概述了几个内置的,
不应在Go项目中使用的名称标识predeclared identifiers

根据上下文的不同,将这些标识符作为名称重复使用,
将在当前作用域(或任何嵌套作用域)中隐藏原始标识符,或者混淆代码。
在最好的情况下,编译器会报错;在最坏的情况下,这样的代码可能会引入潜在的、难以恢复的错误。

BadGood
1
2
3
4
5
6
7
8
var error string
// `error` 作用域隐式覆盖

// or

func handleErrorMessage(error string) {
// `error` 作用域隐式覆盖
}
1
2
3
4
5
6
7
8
var errorMessage string
// `error` 指向内置的非覆盖

// or

func handleErrorMessage(msg string) {
// `error` 指向内置的非覆盖
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Foo struct {
// 虽然这些字段在技术上不构成阴影,但`error`或`string`字符串的重映射现在是不明确的。
error error
string string
}

func (f Foo) Error() error {
// `error` 和 `f.error` 在视觉上是相似的
return f.error
}

func (f Foo) String() string {
// `string` and `f.string` 在视觉上是相似的
return f.string
}
1
2
3
4
5
6
7
8
9
10
11
12
13
type Foo struct {
// `error` and `string` 现在是明确的。
err error
str string
}

func (f Foo) Error() error {
return f.err
}

func (f Foo) String() string {
return f.str
}

注意,编译器在使用预先分隔的标识符时不会生成错误,
但是诸如go vet之类的工具会正确地指出这些和其他情况下的隐式问题。

避免使用 init()

尽可能避免使用init()。当init()是不可避免或可取的,代码应先尝试:

  1. 无论程序环境或调用如何,都要完全确定。
  2. 避免依赖于其他init()函数的顺序或副作用。虽然init()顺序是明确的,但代码可以更改,
    因此init()函数之间的关系可能会使代码变得脆弱和容易出错。
  3. 避免访问或操作全局或环境状态,如机器信息、环境变量、工作目录、程序参数/输入等。
  4. 避免I/O,包括文件系统、网络和系统调用。

不能满足这些要求的代码可能属于要作为main()调用的一部分(或程序生命周期中的其他地方),
或者作为main()本身的一部分写入。特别是,打算由其他程序使用的库应该特别注意完全确定性,
而不是执行“init magic”

BadGood
1
2
3
4
5
6
7
8
9
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}
1
2
3
4
5
6
7
8
9
10
var _defaultFoo = Foo{
// ...
}
// or, 为了更好的可测试性:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
type Config struct {
// ...
}
var _config Config
func init() {
// Bad: 基于当前目录
cwd, _ := os.Getwd()
// Bad: I/O
raw, _ := ioutil.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// handle err
raw, err := ioutil.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// handle err
var config Config
yaml.Unmarshal(raw, &config)
return config
}

考虑到上述情况,在某些情况下,init()可能更可取或是必要的,可能包括:

  • 不能表示为单个赋值的复杂表达式。

  • 可插入的钩子,如database/sql、编码类型注册表等。

  • Google Cloud Functions和其他形式的确定性预计算的优化。

追加时优先指定切片容量

追加时优先指定切片容量

在尽可能的情况下,在初始化要追加的切片时为make()提供一个容量值。

BadGood
1
2
3
4
5
6
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
1
2
3
4
5
6
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
1
BenchmarkBad-4    100000000    2.48s
1
BenchmarkGood-4   100000000    0.21s

主函数退出方式(Exit)

Go程序使用os.Exit 或者 log.Fatal* 立即退出 (使用panic不是退出程序的好方法,请 don’t panic.)

**仅在main()**中调用其中一个 os.Exit 或者 log.Fatal*。所有其他函数应将错误返回到信号失败中。

BadGood
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
body := readFile(path)
fmt.Println(body)
}
func readFile(path string) string {
f, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
b, err := ioutil.ReadAll(f)
if err != nil {
log.Fatal(err)
}
return string(b)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
body, err := readFile(path)
if err != nil {
log.Fatal(err)
}
fmt.Println(body)
}
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
b, err := ioutil.ReadAll(f)
if err != nil {
return "", err
}
return string(b), nil
}

原则上:退出的具有多种功能的程序存在一些问题:

  • 不明显的控制流:任何函数都可以退出程序,因此很难对控制流进行推理。
  • 难以测试:退出程序的函数也将退出调用它的测试。这使得函数很难测试,并引入了跳过 go test 尚未运行的其他测试的风险。
  • 跳过清理:当函数退出程序时,会跳过已经进入defer队列里的函数调用。这增加了跳过重要清理任务的风险。

一次性退出

如果可能的话,你的main()函数中最多一次 调用 os.Exit或者log.Fatal。如果有多个错误场景停止程序执行,请将该逻辑放在单独的函数下并从中返回错误。
这会缩短 main()函数,并将所有关键业务逻辑放入一个单独的、可测试的函数中。

BadGood
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
func main() {
args := os.Args[1:]
if len(args) != 1 {
log.Fatal("missing file")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
log.Fatal(err)
}
defer f.Close()
// 如果我们调用log.Fatal 在这条线之后
// f.Close 将会被执行.
b, err := ioutil.ReadAll(f)
if err != nil {
log.Fatal(err)
}
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
args := os.Args[1:]
if len(args) != 1 {
return errors.New("missing file")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
b, err := ioutil.ReadAll(f)
if err != nil {
return err
}
// ...
}

性能

性能方面的特定准则只适用于高频场景。

优先使用 strconv 而不是 fmt

将原语转换为字符串或从字符串转换时,strconv速度比fmt快。

BadGood
1
2
3
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
}
1
2
3
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
}
1
BenchmarkFmtSprint-4    143 ns/op    2 allocs/op
1
BenchmarkStrconv-4    64.2 ns/op    1 allocs/op

避免字符串到字节的转换

不要反复从固定字符串创建字节 slice。相反,请执行一次转换并捕获结果。

BadGood
1
2
3
for i := 0; i < b.N; i++ {
w.Write([]byte("Hello world"))
}
1
2
3
4
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
w.Write(data)
}
1
BenchmarkBad-4   50000000   22.2 ns/op
1
BenchmarkGood-4  500000000   3.25 ns/op

指定容器容量

尽可能指定容器容量,以便为容器预先分配内存。这将在添加元素时最小化后续分配(通过复制和调整容器大小)。

指定Map容量提示

在尽可能的情况下,在使用 make() 初始化的时候提供容量信息

1
make(map[T1]T2, hint)

make()提供容量提示会在初始化时尝试调整map的大小,这将减少在将元素添加到map时为map重新分配内存。

注意,与slices不同。map capacity提示并不保证完全的抢占式分配,而是用于估计所需的hashmap bucket的数量。
因此,在将元素添加到map时,甚至在指定map容量时,仍可能发生分配。

BadGood
1
2
3
4
5
6
m := make(map[string]os.FileInfo)

files, _ := ioutil.ReadDir("./files")
for _, f := range files {
m[f.Name()] = f
}
1
2
3
4
5
6
7

files, _ := ioutil.ReadDir("./files")

m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
m[f.Name()] = f
}

m 是在没有大小提示的情况下创建的; 在运行时可能会有更多分配。

m 是有大小提示创建的;在运行时可能会有更少的分配。

指定切片容量

在尽可能的情况下,在使用make()初始化切片时提供容量信息,特别是在追加切片时。

1
make([]T, length, capacity)

与maps不同,slice capacity不是一个提示:编译器将为提供给make()的slice的容量分配足够的内存,
这意味着后续的append()`操作将导致零分配(直到slice的长度与容量匹配,在此之后,任何append都可能调整大小以容纳其他元素)。

BadGood
1
2
3
4
5
6
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
1
2
3
4
5
6
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
1
BenchmarkBad-4    100000000    2.48s
1
BenchmarkGood-4   100000000    0.21s

规范

一致性

本文中概述的一些标准都是客观性的评估,是根据场景、上下文、或者主观性的判断;

但是最重要的是,保持一致.

一致性的代码更容易维护、是更合理的、需要更少的学习成本、并且随着新的约定出现或者出现错误后更容易迁移、更新、修复 bug

相反,在一个代码库中包含多个完全不同或冲突的代码风格会导致维护成本开销、不确定性和认知偏差。所有这些都会直接导致速度降低、代码审查痛苦、而且增加 bug 数量。

将这些标准应用于代码库时,建议在 package(或更大)级别进行更改,子包级别的应用程序通过将多个样式引入到同一代码中,违反了上述关注点。

相似的声明放在一组

Go 语言支持将相似的声明放在一个组内。

BadGood
1
2
import "a"
import "b"
1
2
3
4
import (
"a"
"b"
)

这同样适用于常量、变量和类型声明:

BadGood
1
2
3
4
5
6
7
8
9

const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const (
a = 1
b = 2
)

var (
a = 1
b = 2
)

type (
Area float64
Volume float64
)

仅将相关的声明放在一组。不要将不相关的声明放在一组。

BadGood
1
2
3
4
5
6
7
8
type Operation int

const (
Add Operation = iota + 1
Subtract
Multiply
EnvVar = "MY_ENV"
)
1
2
3
4
5
6
7
8
9
type Operation int

const (
Add Operation = iota + 1
Subtract
Multiply
)

const EnvVar = "MY_ENV"

分组使用的位置没有限制,例如:你可以在函数内部使用它们:

BadGood
1
2
3
4
5
6
7
func f() string {
var red = color.New(0xff0000)
var green = color.New(0x00ff00)
var blue = color.New(0x0000ff)

...
}
1
2
3
4
5
6
7
8
9
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)

...
}

import 分组

导入应该分为两组:

  • 标准库
  • 其他库

默认情况下,这是 goimports 应用的分组。

BadGood
1
2
3
4
5
6
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
1
2
3
4
5
6
7
import (
"fmt"
"os"

"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)

包名

当命名包时,请按下面规则选择一个名称:

  • 全部小写。没有大写或下划线。
  • 大多数使用命名导入的情况下,不需要重命名。
  • 简短而简洁。请记住,在每个使用的地方都完整标识了该名称。
  • 不用复数。例如net/url,而不是net/urls
  • 不要用“common”,“util”,“shared”或“lib”。这些是不好的,信息量不足的名称。

另请参阅 Package NamesGo 包样式指南.

函数名

我们遵循 Go 社区关于使用 MixedCaps 作为函数名 的约定。有一个例外,为了对相关的测试用例进行分组,函数名可能包含下划线,如:TestMyFunction_WhatIsBeingTested.

导入别名

如果程序包名称与导入路径的最后一个元素不匹配,则必须使用导入别名。

1
2
3
4
5
6
import (
"net/http"

client "example.com/client-go"
trace "example.com/trace/v2"
)

在所有其他情况下,除非导入之间有直接冲突,否则应避免导入别名。

BadGood
1
2
3
4
5
6
import (
"fmt"
"os"

nettrace "golang.net/x/trace"
)
1
2
3
4
5
6
7
import (
"fmt"
"os"
"runtime/trace"

nettrace "golang.net/x/trace"
)

函数分组与顺序

  • 函数应按粗略的调用顺序排序。
  • 同一文件中的函数应按接收者分组。

因此,导出的函数应先出现在文件中,放在struct, const, var定义的后面。

在定义类型之后,但在接收者的其余方法之前,可能会出现一个 newXYZ()/NewXYZ()

由于函数是按接收者分组的,因此普通工具函数应在文件末尾出现。

BadGood
1
2
3
4
5
6
7
8
9
10
11
12
13
func (s *something) Cost() {
return calcCost(s.weights)
}

type something struct{ ... }

func calcCost(n []int) int {...}

func (s *something) Stop() {...}

func newSomething() *something {
return &something{}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
type something struct{ ... }

func newSomething() *something {
return &something{}
}

func (s *something) Cost() {
return calcCost(s.weights)
}

func (s *something) Stop() {...}

func calcCost(n []int) int {...}

减少嵌套

代码应通过尽可能先处理错误情况/特殊情况并尽早返回或继续循环来减少嵌套。减少嵌套多个级别的代码的代码量。

BadGood
1
2
3
4
5
6
7
8
9
10
11
12
for _, v := range data {
if v.F1 == 1 {
v = process(v)
if err := v.Call(); err == nil {
v.Send()
} else {
return err
}
} else {
log.Printf("Invalid v: %v", v)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
for _, v := range data {
if v.F1 != 1 {
log.Printf("Invalid v: %v", v)
continue
}

v = process(v)
if err := v.Call(); err != nil {
return err
}
v.Send()
}

不必要的 else

如果在 if 的两个分支中都设置了变量,则可以将其替换为单个 if。

BadGood
1
2
3
4
5
6
var a int
if b {
a = 100
} else {
a = 10
}
1
2
3
4
a := 10
if b {
a = 100
}

顶层变量声明

在顶层,使用标准var关键字。请勿指定类型,除非它与表达式的类型不同。

BadGood
1
2
3
var _s string = F()

func F() string { return "A" }
1
2
3
4
5
var _s = F()
// 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定_s 的类型
// 还是那种类型

func F() string { return "A" }

如果表达式的类型与所需的类型不完全匹配,请指定类型。

1
2
3
4
5
6
7
8
type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// F 返回一个 myError 类型的实例,但是我们要 error 类型

对于未导出的顶层常量和变量,使用_作为前缀

在未导出的顶级varsconsts, 前面加上前缀_,以使它们在使用时明确表示它们是全局符号。

例外:未导出的错误值,应以err开头。

基本依据:顶级变量和常量具有包范围作用域。使用通用名称可能很容易在其他文件中意外使用错误的值。

BadGood
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// foo.go

const (
defaultPort = 8080
defaultUser = "user"
)

// bar.go

func Bar() {
defaultPort := 9090
...
fmt.Println("Default port", defaultPort)

// We will not see a compile error if the first line of
// Bar() is deleted.
}
1
2
3
4
5
6
// foo.go

const (
_defaultPort = 8080
_defaultUser = "user"
)

结构体中的嵌入

嵌入式类型(例如 mutex)应位于结构体内的字段列表的顶部,并且必须有一个空行将嵌入式字段与常规字段分隔开。

BadGood
1
2
3
4
type Client struct {
version int
http.Client
}
1
2
3
4
5
type Client struct {
http.Client

version int
}

内嵌应该提供切实的好处,比如以语义上合适的方式添加或增强功能。
它应该在对用户不利影响的情况下完成这项工作(另请参见:避免在公共结构中嵌入类型Avoid Embedding Types in Public Structs)。

嵌入 不应该:

  • 纯粹是为了美观或方便。
  • 使外部类型更难构造或使用。
  • 影响外部类型的零值。如果外部类型有一个有用的零值,则在嵌入内部类型之后应该仍然有一个有用的零值。
  • 作为嵌入内部类型的副作用,从外部类型公开不相关的函数或字段。
  • 公开未导出的类型。
  • 影响外部类型的复制形式。
  • 更改外部类型的API或类型语义。
  • 嵌入内部类型的非规范形式。
  • 公开外部类型的实现详细信息。
  • 允许用户观察或控制类型内部。
  • 通过包装的方式改变内部函数的一般行为,这种包装方式会给用户带来一些意料之外情况。

简单地说,有意识地和有目的地嵌入。一种很好的测试体验是,
“是否所有这些导出的内部方法/字段都将直接添加到外部类型”
如果答案是someno,不要嵌入内部类型-而是使用字段。

BadGood
1
2
3
4
5
type A struct {
// Bad: A.Lock() and A.Unlock() 现在可用
// 不提供任何功能性好处,并允许用户控制有关A的内部细节。
sync.Mutex
}
1
2
3
4
5
6
7
8
9
10
type countingWriteCloser struct {
// Good: Write() 在外层提供用于特定目的,
// 并且委托工作到内部类型的Write()中。
io.WriteCloser
count int
}
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
w.count += len(bs)
return w.WriteCloser.Write(bs)
}
1
2
3
4
5
6
7
8
9
10
type Book struct {
// Bad: 指针更改零值的有用性
io.ReadWriter
// other fields
}
// later
var b Book
b.Read(...) // panic: nil pointer
b.String() // panic: nil pointer
b.Write(...) // panic: nil pointer
1
2
3
4
5
6
7
8
9
10
type Book struct {
// Good: 有用的零值
bytes.Buffer
// other fields
}
// later
var b Book
b.Read(...) // ok
b.String() // ok
b.Write(...) // ok
1
2
3
4
5
6
type Client struct {
sync.Mutex
sync.WaitGroup
bytes.Buffer
url.URL
}
1
2
3
4
5
6
type Client struct {
mtx sync.Mutex
wg sync.WaitGroup
buf bytes.Buffer
url url.URL
}

使用字段名初始化结构体

初始化结构体时,应该指定字段名称。现在由 go vet 强制执行。

BadGood
1
k := User{"John", "Doe", true}
1
2
3
4
5
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}

例外:如果有 3 个或更少的字段,则可以在测试表中省略字段名称。

1
2
3
4
5
6
7
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}

本地变量声明

如果将变量明确设置为某个值,则应使用短变量声明形式 (:=)。

BadGood
1
var s = "foo"
1
s := "foo"

但是,在某些情况下,var 使用关键字时默认值会更清晰。例如,声明空切片。

BadGood
1
2
3
4
5
6
7
8
func f(list []int) {
filtered := []int{}
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
}
1
2
3
4
5
6
7
8
func f(list []int) {
var filtered []int
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
}

nil 是一个有效的 slice

nil 是一个有效的长度为 0 的 slice,这意味着,

  • 您不应明确返回长度为零的切片。应该返回nil 来代替。

    BadGood
    1
    2
    3
    if x == "" {
    return []int{}
    }
    1
    2
    3
    if x == "" {
    return nil
    }
  • 要检查切片是否为空,请始终使用len(s) == 0。而非 nil

    BadGood
    1
    2
    3
    func isEmpty(s []string) bool {
    return s == nil
    }
    1
    2
    3
    func isEmpty(s []string) bool {
    return len(s) == 0
    }
  • 零值切片(用var声明的切片)可立即使用,无需调用make()创建。

    BadGood
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    nums := []int{}
    // or, nums := make([]int)

    if add1 {
    nums = append(nums, 1)
    }

    if add2 {
    nums = append(nums, 2)
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var nums []int

    if add1 {
    nums = append(nums, 1)
    }

    if add2 {
    nums = append(nums, 2)
    }

记住,虽然nil切片是有效的切片,但它不等于长度为0的切片(一个为nil,另一个不是),并且在不同的情况下(例如序列化),这两个切片的处理方式可能不同。

缩小变量作用域

如果有可能,尽量缩小变量作用范围。除非它与 减少嵌套的规则冲突。

BadGood
1
2
3
4
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
return err
}
1
2
3
if err := ioutil.WriteFile(name, data, 0644); err != nil {
return err
}

如果需要在 if 之外使用函数调用的结果,则不应尝试缩小范围。

BadGood
1
2
3
4
5
6
7
8
9
10
11
if data, err := ioutil.ReadFile(name); err == nil {
err = cfg.Decode(data)
if err != nil {
return err
}

fmt.Println(cfg)
return nil
} else {
return err
}
1
2
3
4
5
6
7
8
9
10
11
data, err := ioutil.ReadFile(name)
if err != nil {
return err
}

if err := cfg.Decode(data); err != nil {
return err
}

fmt.Println(cfg)
return nil

避免参数语义不明确(Avoid Naked Parameters)

函数调用中的意义不明确的参数可能会损害可读性。当参数名称的含义不明显时,请为参数添加 C 样式注释 (/* ... */)

BadGood
1
2
3
// func printInfo(name string, isLocal, done bool)

printInfo("foo", true, true)
1
2
3
// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)

对于上面的示例代码,还有一种更好的处理方式是将上面的 bool 类型换成自定义类型。将来,该参数可以支持不仅仅局限于两个状态(true/false)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Region int

const (
UnknownRegion Region = iota
Local
)

type Status int

const (
StatusReady Status= iota + 1
StatusDone
// Maybe we will have a StatusInProgress in the future.
)

func printInfo(name string, region Region, status Status)

使用原始字符串字面值,避免转义

Go 支持使用 原始字符串字面值,也就是 “ ` “ 来表示原生字符串,在需要转义的场景下,我们应该尽量使用这种方案来替换。

可以跨越多行并包含引号。使用这些字符串可以避免更难阅读的手工转义的字符串。

BadGood
1
wantError := "unknown name:\"test\""
1
wantError := `unknown error:"test"`

初始化结构体

使用字段名初始化结构

初始化结构时,几乎应该始终指定字段名。目前由go vet强制执行。

BadGood
1
k := User{"John", "Doe", true}
1
2
3
4
5
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}

例外:当有3个或更少的字段时,测试表中的字段名may可以省略。

1
2
3
4
5
6
7
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}

省略结构中的零值字段

初始化具有字段名的结构时,除非提供有意义的上下文,否则忽略值为零的字段。
也就是,让我们自动将这些设置为零值

BadGood
1
2
3
4
5
6
user := User{
FirstName: "John",
LastName: "Doe",
MiddleName: "",
Admin: false,
}
1
2
3
4
user := User{
FirstName: "John",
LastName: "Doe",
}

这有助于通过省略该上下文中的默认值来减少阅读的障碍。只指定有意义的值。

在字段名提供有意义上下文的地方包含零值。例如,表驱动测试 中的测试用例可以受益于字段的名称,即使它们是零值的。

1
2
3
4
5
6
7
tests := []struct{
give string
want int
}{
{give: "0", want: 0},
// ...
}

对零值结构使用 var

如果在声明中省略了结构的所有字段,请使用 var 声明结构。

BadGood
1
user := User{}
1
var user User

这将零值结构与那些具有类似于为[初始化 Maps]创建的,区别于非零值字段的结构区分开来,
并与我们更喜欢的declare empty slices方式相匹配。

初始化 Struct 引用

在初始化结构引用时,请使用&T{}代替new(T),以使其与结构体初始化一致。

BadGood
1
2
3
4
5
sval := T{Name: "foo"}

// inconsistent
sptr := new(T)
sptr.Name = "bar"
1
2
3
sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

初始化 Maps

对于空 map 请使用 make(..) 初始化, 并且 map 是通过编程方式填充的。
这使得 map 初始化在表现上不同于声明,并且它还可以方便地在 make 后添加大小提示。

BadGood
1
2
3
4
5
6
var (
// m1 读写安全;
// m2 在写入时会 panic
m1 = map[T1]T2{}
m2 map[T1]T2
)
1
2
3
4
5
6
var (
// m1 读写安全;
// m2 在写入时会 panic
m1 = make(map[T1]T2)
m2 map[T1]T2
)

声明和初始化看起来非常相似的。

声明和初始化看起来差别非常大。

在尽可能的情况下,请在初始化时提供 map 容量大小,详细请看 指定Map容量提示

另外,如果 map 包含固定的元素列表,则使用 map literals(map 初始化列表) 初始化映射。

BadGood
1
2
3
4
m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3
1
2
3
4
5
m := map[T1]T2{
k1: v1,
k2: v2,
k3: v3,
}

基本准则是:在初始化时使用 map 初始化列表 来添加一组固定的元素。否则使用 make (如果可以,请尽量指定 map 容量)。

字符串 string format

如果你在函数外声明Printf-style 函数的格式字符串,请将其设置为const常量。

这有助于go vet对格式字符串执行静态分析。

BadGood
1
2
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
1
2
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

命名 Printf 样式的函数

声明Printf-style 函数时,请确保go vet可以检测到它并检查格式字符串。

这意味着您应尽可能使用预定义的Printf-style 函数名称。go vet将默认检查这些。有关更多信息,请参见 Printf 系列

如果不能使用预定义的名称,请以 f 结束选择的名称:Wrapf,而不是Wrapgo vet可以要求检查特定的 Printf 样式名称,但名称必须以f结尾。

1
$ go vet -printfuncs=wrapf,statusf

另请参阅 go vet: Printf family check.

编程模式

表驱动测试

当测试逻辑是重复的时候,通过 subtests 使用 table 驱动的方式编写 case 代码看上去会更简洁。

BadGood
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// func TestSplitHostPort(t *testing.T)

host, port, err := net.SplitHostPort("192.0.2.0:8000")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "8000", port)

host, port, err = net.SplitHostPort("192.0.2.0:http")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "http", port)

host, port, err = net.SplitHostPort(":8000")
require.NoError(t, err)
assert.Equal(t, "", host)
assert.Equal(t, "8000", port)

host, port, err = net.SplitHostPort("1:8")
require.NoError(t, err)
assert.Equal(t, "1", host)
assert.Equal(t, "8", port)
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
// func TestSplitHostPort(t *testing.T)

tests := []struct{
give string
wantHost string
wantPort string
}{
{
give: "192.0.2.0:8000",
wantHost: "192.0.2.0",
wantPort: "8000",
},
{
give: "192.0.2.0:http",
wantHost: "192.0.2.0",
wantPort: "http",
},
{
give: ":8000",
wantHost: "",
wantPort: "8000",
},
{
give: "1:8",
wantHost: "1",
wantPort: "8",
},
}

for _, tt := range tests {
t.Run(tt.give, func(t *testing.T) {
host, port, err := net.SplitHostPort(tt.give)
require.NoError(t, err)
assert.Equal(t, tt.wantHost, host)
assert.Equal(t, tt.wantPort, port)
})
}

很明显,使用 test table 的方式在代码逻辑扩展的时候,比如新增 test case,都会显得更加的清晰。

我们遵循这样的约定:将结构体切片称为tests。 每个测试用例称为tt。此外,我们鼓励使用givewant前缀说明每个测试用例的输入和输出值。

1
2
3
4
5
6
7
8
9
10
11
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}

for _, tt := range tests {
// ...
}

功能选项

功能选项是一种模式,您可以在其中声明一个不透明 Option 类型,该类型在某些内部结构中记录信息。您接受这些选项的可变编号,并根据内部结构上的选项记录的全部信息采取行动。

将此模式用于您需要扩展的构造函数和其他公共 API 中的可选参数,尤其是在这些功能上已经具有三个或更多参数的情况下。

BadGood
1
2
3
4
5
6
7
8
9
// package db

func Open(
addr string,
cache bool,
logger *zap.Logger
) (*Connection, error) {
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// package db

type Option interface {
// ...
}

func WithCache(c bool) Option {
// ...
}

func WithLogger(log *zap.Logger) Option {
// ...
}

// Open creates a connection.
func Open(
addr string,
opts ...Option,
) (*Connection, error) {
// ...
}

必须始终提供缓存和记录器参数,即使用户希望使用默认值。

1
2
3
4
db.Open(addr, db.DefaultCache, zap.NewNop())
db.Open(addr, db.DefaultCache, log)
db.Open(addr, false /* cache */, zap.NewNop())
db.Open(addr, false /* cache */, log)

只有在需要时才提供选项。

1
2
3
4
5
6
7
8
db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false))
db.Open(
addr,
db.WithCache(false),
db.WithLogger(log),
)

Our suggested way of implementing this pattern is with an Option interface
that holds an unexported method, recording options on an unexported options
struct.

我们建议实现此模式的方法是使用一个 Option 接口,该接口保存一个未导出的方法,在一个未导出的 options 结构上记录选项。

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
44
45
46
47
type options struct {
cache bool
logger *zap.Logger
}

type Option interface {
apply(*options)
}

type cacheOption bool

func (c cacheOption) apply(opts *options) {
opts.cache = bool(c)
}

func WithCache(c bool) Option {
return cacheOption(c)
}

type loggerOption struct {
Log *zap.Logger
}

func (l loggerOption) apply(opts *options) {
opts.logger = l.Log
}

func WithLogger(log *zap.Logger) Option {
return loggerOption{Log: log}
}

// Open creates a connection.
func Open(
addr string,
opts ...Option,
) (*Connection, error) {
options := options{
cache: defaultCache,
logger: zap.NewNop(),
}

for _, o := range opts {
o.apply(&options)
}

// ...
}

注意: 还有一种使用闭包实现这个模式的方法,但是我们相信上面的模式为作者提供了更多的灵活性,并且更容易对用户进行调试和测试。特别是,在不可能进行比较的情况下它允许在测试和模拟中对选项进行比较。此外,它还允许选项实现其他接口,包括 fmt.Stringer,允许用户读取选项的字符串表示形式。

还可以参考下面资料:

Linting

比任何 “blessed” linter 集更重要的是,lint在一个代码库中始终保持一致。

我们建议至少使用以下linters,因为我认为它们有助于发现最常见的问题,并在不需要规定的情况下为代码质量建立一个高标准:

Lint Runners

我们推荐 golangci-lint 作为go-to lint的运行程序,这主要是因为它在较大的代码库中的性能以及能够同时配置和使用许多规范。这个repo有一个示例配置文件.golangci.yml和推荐的linter设置。

golangci-lint 有various-linters可供使用。建议将上述linters作为基本set,我们鼓励团队添加对他们的项目有意义的任何附加linters。

Stargazers over time

Stargazers over time

  • Copyrights © 2023-2024 杨海波
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信