Fork me on GitHub

RobPike谈GoogGo

1. Rob,你创建了Google Go这门语言。什么是Google Go?能简明扼要的介绍一下Google Go吗?

我还是讲讲为什么要创建这门语言吧,和你的问题稍有些不同。我在Google做了一个有关编程语言的系列讲座,在Youtube上有,谈及了我早期所写的一个语言,叫做Newsqueak,那是八十年代的事,非常早。在做讲座期间,我开始思考为什么Newsqueak中的一些想法在我现在以C++为主的工作环境中无法使用。而且在Google我们经常要构建非常大的程序,光构建就要花很多时间,对依赖的管理也有问题,由于链接了本来并不需要的东西,二进制程序包变得很大,链接时间很长,编译时间也很长,而且C++的工作方式有点古老,其底层实际上C,C++已经有三十年的历史了,而C则更是有四十年了。用现今的硬件做计算,有很多新东西需要考虑:多核机器、网络化、分布式系统、云计算等等。

2. Go的主要特点是什么?有什么重要功能?

对于大多数人来说,他们对Go的第一印象是该语言将并发性作为语言原语,这对我们处理分布式计算和多核这类东西来说非常好、也非常重要。我猜许多人会认为Go是一门简单无趣的语言,没有什么特别的东西,因为其构想看起来一目了然。但实际上不能用第一印象来判断Go。很多用过Go的人会发现它是一门非常高产而且有表现力的语言,能够解决我们编写这门语言时期望其所能解决的所有问题。

Go的编译过程很快,二进制程序包又比较小,它管理依赖的方式如同管理语言本身的东西一样。这里还有一个故事呢,但是在这里就不再展开讨论了,但是这门语言的并发性使其能够以非常简单的模式来处理非常复杂的操作及分布式计算环境。我想最重要的功能可能就是并发性了,后面我们可以谈谈该语言的类型系统,其与C++、Java这类传统面向对象类型系统的差异很大。

3. 在我们继续话题之前,能否解释一下为什么Go编译器能达到那么快的编译速度呢?有什么法宝?

它之所以快,有两个原因。首先Go有两个编译器——两个单独的实现。一个是按照Plan 9(http://plan9.bell-labs.com/wiki/plan9/1/) 风格新写的编译器,它有自己独特的工作方式,是个全新的编译器。另一个编译器叫做GCC Go,它拥有GCC前端,这个编译器是Ian Taylor后来写的。所以Go有两个编译器,速度快是二者的共同特点,但是Plan 9风格编译器的速度是GCC Go的5倍,因为它从头到脚都是全新的,没有GCC后端,那些东西会花很多时间来产生真正的好代码。

GCC Go编译器要产生更好的代码,所以速度慢些。不过真正重要的一点是Go编译器的依赖管理特性才是其编译速度快的真正原因。如果你去看一个C或C++程序,便会发现其头文件描述了函数库、对象代码等等东西。语言本身并不强制检查依赖,每一次你都必须分析代码以便清楚你的函数是怎样的。如果你编译过程中想用另一个类的C++程序,你必须先编译它所依赖的类和头文件等等等等。如果你所编译的C++程序有许多类,并且内部相关,你可能会把同一个头文件编译数百次甚至上千次。当然,你可以用预编译头文件及其他技巧来回避之一问题。

但是语言本身并不能帮上你的忙,工具可能会让这一问题得到改善,可是最大的问题是并没有什么能保证你所编译的东西就是程序真正需要的东西。有可能你的程序包含了一个并不真正需要的头文件,但是你没办法知道,因为语言并没有强制检查。而Go有一个更加严格的依赖模型,它有一些叫做包(packages)的东西,你可以把它想象成Java类文件或着类似的东西,或者函数库什么的,虽然他们并不相同,但基本思路是一样的。关键问题是,如果这个东西依赖那个东西,而那个东西又依赖另外一个东西,比如A依赖于B,B又依赖于C,那么你必须首先编译最内层的依赖:即,你先编译C,然后编译B,最后编译A。

但是如果A依赖B,但是A并不直接依赖于C,而是存在依赖传递,那么该怎么办呢?这时所有B需要从C拿到的信息都会被放在B的对象代码里。这样,当我编译A的时候,我不需要再管C了。于是事情就非常简单了:在你编译程序时,你只需将类型信息沿着依赖关系树向上遍历即可,如果你到达树的顶端,则只需编译紧邻的依赖,而不用管其它层级的依赖了。如果你要做算术运算,你会发现在Objective-C或C++或类似的语言里,虽然只包含了一个简单的头文件,但由于依赖传递的存在,你可能会编译数十万行程序。然而在Go中,你打开一个文件,里面或许只有20行,因为其中只描述了公共接口。

如果一个依赖链里只有三个文件,Go的优势可能并不明显,但是如果你有成千上万个文件的时候,Go的速度优势会成指数增长。我们相信,如果用Go的话,我们应该能够在数秒内就编译完数百万行代码。然而如果是等量的用C++编写的程序,由于依赖管理问题,编译的开销会大得多,编译的时间将会长达若干分钟。因此,Go速度快的根源主要归功于对依赖的管理。

4. 让我们开始聊聊Go里的类型系统吧。Go里面有结构(struct)、有类型(type),那么Go里的类型是什么?

Go里的类型与其它传统编程语言里的类型是类似的。Go里的类型有整数、字符串、struct数据结构、以及数组(array),我们称之为切片(slice),它们类似于C的数组,但更易于使用,更加固定一些。你可以声明本地类型并予以命名,然后按照通常的方式来使用。Go和面向对象方式的不同之处在于,类型只是书写数据的一种方式,方法则是一个完全独立的概念。你可以把方法放在struct上,在Go里没有类的概念,取而代之的是结构,以及为此结构声明的一些方法。

结构不能与类混为一谈。但是你也可以把方法放在数组、整数、浮点数或字符串上,实际上任何类型都可以有方法。因此,这里方法的概念比Java的方法更加泛化,在Java里方法是类的一部分,仅此而已。例如,你的整数上可以有方法,听上去似乎没什么用,但是如果你想在一个叫做Tuesday的整数常量上附加上to_string方法来打印出漂亮的星期格式;或者,你想重新格式化字符串使其能够以不同的方式打印出自己,这时你就会意识到它的作用。为什么非要把所有方法或者其它好东西都塞进类里面呢,为什么不让它们提供更广泛的服务呢?

5. 那么这些方法只是在包内部可见喽?

非也,实际上是这样,Go只允许你在包内为你所实现的类型定义方法。我不能引入你的类型然后直接把我的方法增加进去,但是我可以使用匿名属性(anonymous field)将其包裹起来,方法可不是你想加到哪就加到哪的,你要定义类型,然后才能把方法放在上面。正因为如此,我们在包里提供了另一种封装——接口(interface),但是如果你不明白谁能为对象增加方法的严格界限,就很难理解接口。

6. 你的意思是,我可以给int增加方法,但是必须先使用typedef吗?

你要typedef一个整数类型,起个名字,如果你正在处理一星期中的七天,可以就叫它“Day”,你可以给你所声明的类型——Day增加方法,但是你不能直接给int增加方法。因为整数类型不是你定义的,不在你的包里,它是引入的但并不在你的包中定义,这就意味着你不能给其增加方法。你不能给不在你包里定义的类型增加方法。

7. 你们借鉴了Ruby里开放类的思想,这很有意思。Ruby的开放类实际上是可以修改类并增加新的方法,这是有破坏性的,但是你们的方法本质上是安全的,因为创建了新的东西。

它是安全可控的,而且很容易理解。最初我们觉得类型用起来可能不太方便,我们也希望像Ruby那样添加方法,但这又让接口比较难以理解。所以,我们只把方法取出来,而不是放进去,我们想不出有什么更好的办法,于是限制方法只能在本地类型上,不过这种思路确实很容易理解和使用。

8. 你还提到了typedef,是叫typedef吧?

应该叫“type”,你所说的类型——Day的定义方式是这样“type Day int”,这样你就有一个新类型了,你可以在其上增加方法、声明变量,但这个类型不同于int,不像C那样,只是同一事物另起了个名字而已,在Go里实际上你创建了一个不同于int的新类型,叫做“Day”,它拥有int的结构特性,但却有自己的方法集。

9. Typedef在C里是一种预处理指令吗?【编辑注/免责申明:C语言里的typedef与预处理无关】

那实际上就是个别名,但在Go里不是别名,是新类型。

10. 我们从底层说起吧,在Go里最小的类型是什么?

最小的类型应该是布尔类型(bool)吧。bool、int和float,然后是int32、float64之类有尺寸的类型、字符串、复杂类型,可能有遗漏,但这就是基本类型集了。你可以由这些类型构建结构、数组、映射(map),映射在Go里是内建类型不是函数库。然后我想就该是接口了,到了接口,有趣的东西才真正开始。

11. 但是,int这样的类型是值类型对吧.

Int是值类型。在Go里,任何类型都是值类型,和C一样,所有东西都是按值调用,但是你也可以用指针。如果你想引用某样东西,可以获取其地址,这样你就有了一个指针。Go也有指针但是比C指针有更多限制,Go里的指针是安全的,因为他们是类型安全的,所以你没法欺骗编译器,而且也没有指针运算,因此,如果你有个指向某物的指针,你无法将其移到对象外,也无法欺骗编译器。

12. 它们类似C++的引用吗?

是的,很像引用,但是你可以按照你预期的方式对它们进行写操作。而且你可以使用结构内部(如缓冲区)中间的某个地址,它和Java的引用不一样。在Java中,你必须在旁边分配一个缓冲区,这是额外的开销。在Go中,你实际上把该对象分配为结构的一部分,在同一内存块中,这对性能是非常重要的。

13. 它是结构内部一个复合对象。

是的,如果它是值而不是指针的话,是这样。当然你也可以把指针放在结构内部和外部,但是如果你有struct A,而把struct B放在struct A里,那么stuct B就是一块内存,而不像Java那样,这也是Java性能问题的原因之一。

14. 你提到过接口比较有趣,那下面咱们就谈谈这一部分。

Go里的接口真的非常、非常地简单。接口指明了两个不同事情:其一,它表明了类型的构思,接口类型是一个罗列了一组方法的类型,因此如果你要抽象一组方法来定义一个行为,那么就定义一个接口并声明这些方法。现在你就有了一个类型,我们就叫它接口类型吧,那么从现在起所有实现了接口中这些方法的类型——包括基本类型、结构、映射(map)或其它什么类型,都隐含符合该接口要求。其二,也是真正有意思的是,和大多数语言中的接口不同的是,Go里面没有“implements”声明。

你无须说明“我的对象实现了这个接口”,只要你定义了接口中的那些方法,它就自动实现了该接口。有些人对此感到非常担忧,依我看他们想说的是:知道自己实现(Implement)了什么接口真的很重要。如果你真想确定自己实现了什么接口,还是有技巧可以做到这一点的。但是我们的想法与此截然不同,我们的想法是你不应该考虑实现什么接口,而是应该写下要做的东西,因为你不必事前就决定要实现哪个接口。可能后来你实际上实现了某个现在你尚不知晓的接口,因为该接口还未设计出来,但是现在你已经在实现它。

后来你可能发现两个原先未曾考虑过相关性的类具有了相关性——我又用了类这个词,我思考Java太多了——两个structs都实现了一些非常有用的小子集中的相关方法,这时有办法能够操作这两个structs中的任意一个就显得非常有用了。这样你就可以声明一个接口,然后什么都不用管了,即使这些方法是在别人的代码中实现的也没问题,虽然你不能编辑这些代码。如果是Java,这些代码必须要声明实现你的接口,在某种意义上,实现是单向的。然而在Go里,实现是双向的。对于接口实际上有不少漂亮而简单的例子。

我最爱用的一个真实例子就是“Reader”,Go里有个包叫做IO,IO包里有个Reader接口,它只有一个方法,该方法是read方法的标准声明,比如从操作系统或文件中读取内容。这个接口可以被系统中任何做read系统调用的东西所实现。显然,文件、网络、缓存、解压器、解密机、管道,甚至任何想访问数据的东西,都可以给其数据提供一个Reader接口,然后想从这些资源中读取数据的任何程序都可以通过该接口达到目的。这有点像我们前面说过的Plan 9,但是用不同的方式泛化的。

与之类似,Writer也是比较好理解的另一个例子,Writer 由那些要做写操作的人来实现。那么在做格式化打印时,fpringf的第一参数不是file了,而是Writer。这样,fprintf可以给任何实现了write方法的东西做IO格式化的工作。有很多很好的例子:比如HTTP,如果你正在实现一个HTTP服务器,你仅须对connection做fprintf,便可将数据传递到客户端,不需要任何花哨的操作。你可以通过压缩器来进行写操作,你可以通过我所提到的任何东西来进行写操作:压缩器、加密机、缓存、网络连接、管道、文件,你都可以通过fprintf直接操作,因为它们都实现了write方法,因此,隐含都隐含符合writer接口要求。

15. 某种程度上有点类似结构化类型系统(structural typing)

不考虑它的行为的话,它是有点像结构化类型系统。不过它是完全抽象的,其意并不在拥有什么,而是能做什么。有了结构(struct)之后,就规定了其内存的样子,然后方法说明了结构的行为,再之后,接口则抽象了该结构及其它实现了相同方法的其他结构中的这些方法。这是一种鸭子类型系统(duck typing,一种动态类型系统,http://en.wikipedia.org/wiki/Duck_typing),而不是结构化类型系统。

16. 你提到过类,但Go没有类,对吧。

Go没有类。

17. 但是没有类怎么去写代码?

带方法的结构(stuct)很像是类。比较有意思的不同之处是,Go没有子类型继承,你必须学习Go的另类写法,Go有更强大、更有表现力的东西。不过Java程序员和C++程序员刚开始使用Go的时候会感到意外,因为他们实际上在用Go去编写Java程序或C++程序,这样的代码工作得并不好,你可以这样做,但这样就略显笨拙了。但是如果你退一步,对自己说“我该怎样用Go去编写这些东西呢?”,你会发现模式其实是不同的,用Go你可以用更短的程序来表达类似的想法,因为你不需要在所有子类里重复实现行为。这是个非常不同的环境,比你第一眼看上去的还要不同。

18. 如果我有一些行为要实现,而且想放在多个structs里,怎么去共享这些行为?

有一个叫做匿名域的概念,也就是所谓的嵌入。其工作方式是这样:如果你有一个结构(struct),而又有一些其它东西实现了你想要的行为,你可以把这些东西嵌入到你的结构(struct)里,这样,这个结构(struct)不仅仅可以获得被嵌入者的数据还可以获得它的方法。如果你有一些公共行为,比如某些类型里都有一个name方法,在Java里的话你会认为这是一组子类(继承来的方法),在Go里,你只需拿到一个拥有name方法的类型,放在所有你要实现这个方法的结构里,它们就会自动获得name方法,而不用在每个结构里都去写这个方法。这是个很简单的例子,但有不少有趣的结构化的东西使用到了嵌入。

而且,你还可以把多个东西嵌入到一个单一结构中,你可以把它想象成多重继承,不过这会让人更加迷惑,实际在Go里它是很简单的,它只是一个集合,你可以放任何东西在里面,基本上联合了所有的方法,对每个方法集合,你只需写一行代码就可以拥有其所有行为。

19. 如果有多重继承命名冲突的问题该怎么办?

命名冲突实际上并没什么,Go是静态处理这一问题的。其规则是,如果有多层嵌入,则最高层优先;如果同一层有两个相同的名字或相同的方法,Go会给出一个简单的静态错误。你不用自己检查,只需留意这个错误即可。命名冲突是静态检查的,而且规则非常简单,在实践中命名冲突发生的也并不多。

20. 因为系统中没有根对象或根类,如果我想得到一个拥有不同类型的结构的列表,应该怎么办?

接口一个有意思的地方是他们只是集合,方法的集合,那么就会有空集合,没有任何方法的接口,我们称之为空接口。系统中任何东西都符合空接口的要求。空接口有点类似于Java的Object,不同之处在于,int、float和string也符合空接口,Go并不需要一个实际的类,因为Go里没有类的概念,所有东西都是统一的,这有点像void,只不过void是针对指针而不是值。

但是一个空接口值可以代表系统中的任何东西,非常具有普遍性。所以,如果创建一个空接口数组,实际上你就有了一个多态性容器,如果你想再把它拿出来,Go里面有类型开关,你可以在解包的时候询问里面的类型,因此可以安全的进行解包操作。

21. Go里有叫做Goroutines的东西,它们和coroutines有什么区别?不一样么?

Coroutines和Goroutines是不同的,它们的名字反应了这一点。我们给它起了个新名,因为有太多术语了,进程(processes)、线程(threads)、轻量级线程、弦(chords),这些东西有数不清的名字,而Goroutines也并不新鲜,同样的概念在其它系统里已经都有了。但是这个概念和前面那些名字有很大不同,我希望我们自己起名字来命名它们。Goroutine背后的含义是:它是一个coroutine,但是它在阻塞之后会转移到其它coroutine,同一线程上的其它coroutines也会转移,因此它们不会阻塞。

因此,从根本上讲Goroutines是coroutines的一个分支,可在足够多的操作线程上获得多路特性,不会有Goroutines会被其他coroutine阻塞。如果它们只是协作的话,只需一个线程即可。但是如果有很多IO操作的话,就会有许多操作系统动作,也就会有许多许多线程。但是Goroutines还是非常廉价的,它们可以有数十万之众,总体运行良好并只占用合理数量的内存,它们创建起来很廉价并有垃圾回收功能,一切都非常简单。

22. 你提到你们使用了m:n线程模型,即m个coroutines映射到n个线程上?

对的,但是coroutines的数量和线程的数量是按照程序所做工作动态决定的。

23. Goroutines有用于通信的通道吗?

是的,一旦有两个独立执行的功能,如果Goroutine们要相互协作它们就需要相互对话。所以就有了通道这个概念,它实际上是一个类型消息队列,你可以用它来发送值,如果你在Goroutine中持有通道的一端,那么你可以发送类型值给另外一端,那一端则会得到想要的东西。通道有同步和异步之分,我们尽可能使用同步通道,因为同步通道的构思非常好,你可以同时进行同步和通信,所有东西运行起来都步调一致。

但是有时由于效率原因或调度原因,对消息进行缓存也是有意义的。你可以向通道发送整型消息、字符串、结构、指向结构的指针等任何东西,非常有意思的事,你可以在通道上发送另一个通道。这样,我就能够把与他人的通信发送给你,这是非常有意思的概念。

24. 你提到你们有缓存的同步通道和异步通道。

不对,同步是没有缓存的;异步和缓存是一个意思,因为有了缓存,我才能把值放在缓存的空间里进行保存。但是如果没有缓存,我必须等着别人把值拿走,因此无缓存和同步是一个意思。

25. 每个Goroutine就像是一个小的线程,可以这么给读者解释吧。

对,但是轻量级的。

26. 它们是轻量级的。但是每个线程同样都预分配栈空间,因而它们非常耗费资,Goroutines是怎么处理的呢?

没错,Goroutines在被创建的时候,只有非常小的一个栈——4K,可能有点小吧,这个栈是在堆中的,当然,你知道如果在C语言里有这么一个小栈会发生什么,当你调用函数或分配数组之类的东西时,程序会马上溢出。在Go里则不会发生这样的事情,每个函数的开头都会有若干指令以检查栈指针是否达到其界限,如果到达界限,它会链接到其它块上,这种连接的栈叫做分段栈,如果你使用了比刚开始启动时更多的栈,你就有了这种栈块链接串,我们称之为分段栈。

由于只有若干指令,这种机制非常廉价。当然,你可以分配多个栈块,但是Go编译器更倾向于将大的东西移到堆上,因此实际上典型的用法是,你必须在达到4K边界之前调用几个方法,虽然这并不经常发生。但是有一点很重要:它们创建起来很廉价,因为仅有一次内存分配,而且分配的内存非常小,在创建一个新的Goroutine时你不用指明栈的尺寸,这是很好的一种抽象,你根本不用担心栈的大小问题。之后,栈会随需求增长或缩小,你不用担心递归会有问题,你也不用担心大的缓存或任何对程序员完全不可见的东西,一切由Go语言来打理,这是一门语言的整体构思。

27. 我们再来谈谈自动化方面的东西,最初你们是将Go语言作为系统级语言来推广的,一个有趣的选择是使用了垃圾回收器,但是它速度并不快或者说有垃圾回收间歇问题,如果用它写一个操作系统的话,这是非常烦人的。你们是怎么看这一问题的?

我认为这是个非常难的问题,我们也还没有解决它,我们的垃圾回收器可以工作,但是有一些延迟问题,垃圾回收器可能会停顿,但是我们的看法是,我们相信尽管这是一个研究课题,虽还没解决但是我们正在努力。对于现今的并行机,通过把机器内核的一些碎片专门分给作为后台任务的垃圾回收来进行并行回收是可行的。在这一领域有很多工作要做,也取得了不少成功,但这是个很微妙的问题,我不认为而我们会把延迟降为0,但是我相信我们可以让延迟尽可能低,这样对于绝大多数系统软件来讲它不再是个问题。我不保证每个程序都不会有显著延迟,但是我想我们可以获得成功,而且这是Go语言中一个比较活跃的领域。

28. 有没有方法能够避免直面垃圾回收器,比如用一些大容量缓存,我们可以把数据扔进去。

Go可以让你深入到内存布局,你可以分配自己的空间,如果你想的话可以自己做内存管理。虽然没有alloc和free方法,但是你可以声明一个缓存把东西放进去,这个技巧可用来避免产生不必要的垃圾。就像在C语言一样,在C里,如果你老是malloc和free,代价很大。因此,你分配一个对象数组并把它们链接在一起,形成一个链表,管理你自己的空间,而且还不用malloc和free,那么速度会很快。你可以做与Go所做相同的事情,因为Go赋予你与底层事物安全打交道的能力,因此不用欺骗类型系统来达到目的,你实际上可以自己来做。

前面我表达了这样的观点,在Java里,无论何时你在结构里嵌入其它东西,都是通过指针来实现的,但在Go里你可以把它放在一个单一结构中。因此如果你有一些需要若干缓存的数据结构,你可以把缓存放在结构的内存里,这不仅意味着高效(因为你不用间接得到缓存),而且还意味着单一结构可以在一步之内进行内存分配与垃圾回收。这样开销就会减少。因此,如果你考虑一下垃圾回收的实际情况,当你正在设计性能要求不高的东西时,你不应该总是考虑这个问题。但如果是高性能要求的,考虑到内存布局,尽管Go是具有真正垃圾回收特性的语言,它还是给了你工具,让你自己来控制有多少内存和产生了的垃圾。我想这是很多人容易忽略的。

29. 最后一个问题:Go是系统级语言还是应用级语言?

我们是把他设计为一种系统级语言,因为我们在Google所做的工作是系统级的,对吧?Web服务器和数据库系统、以及存储系统等,这些都是系统。但不是操作系统,我不知道Go是否能成为一个好的操作系统语言,但是也不能说它不会成为这样的语言。有趣的是由于我们设计语言时所采用的方法,Go最终成为了一个非常好的通用语言,这有点出乎我们意料。我想大多数用户并没有实际从系统观点来考虑过它,尽管很多人做过一点Web服务器或类似东西。

Go用来做很多应用类的东西也非常不错,它将会有更好的函数库,越来越多的工具以及一些Go更有用的东西,Go是一个非常好的通用语言,它是我用过的最高产的语言。

Go自带库的使用说明

Go 中的时间操作

Golang中与时间有关的操作,主要涉及到 time 包,核心数据结构是 time.Time,如下:

1
2
3
4
5
type Time struct {
wall uint64
ext int64
loc *Location
}

1、获取时间相关函数

1.1 获取当前时间

1
2
3
4
5
6
7
8
9
// 返回当前时间,注意此时返回的是 time.Time 类型
now := time.Now()
fmt.Println(now)
// 当前时间戳
fmt.Println(now.Unix())
// 纳秒级时间戳
fmt.Println(now.UnixNano())
// 时间戳小数部分 单位:纳秒
fmt.Println(now.Nanosecond())

输出:

1
2
3
4
2021-01-10 14:56:15.930562 +0800 CST m=+0.000124449
1610261775
1610261775930562000
930562000

1.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
26
27
now := time.Now()
// 返回日期
year, month, day := now.Date()
fmt.Printf("year:%d, month:%d, day:%d\n", year, month, day)
// 年
fmt.Println(now.Year())
// 月
fmt.Println(now.Month())
// 日
fmt.Println(now.Day())
// 时分秒
hour, minute, second := now.Clock()
fmt.Printf("hour:%d, minute:%d, second:%d\n", hour, minute, second)
// 时
fmt.Println(now.Hour())
// 分
fmt.Println(now.Minute())
// 秒
fmt.Println(now.Second())
// 返回星期
fmt.Println(now.Weekday())
//返回一年中对应的第几天
fmt.Println(now.YearDay())
//返回时区
fmt.Println(now.Location())
// 返回一年中第几天
fmt.Println(now.YearDay())

1.3 格式化时间

Go 语言提供了时间类型格式化函数 Format(),需要注意的是 Go 语言格式化时间模板不是常见的 Y-m-d H:i:s,而是 2006-01-02 15:04:05,也很好记忆(2006 1 2 3 4 5)。

1
2
3
4
5
6
now := time.Now()
fmt.Println(now.Format("2006-01-02 15:04:05"))
fmt.Println(now.Format("2006-01-02"))
fmt.Println(now.Format("15:04:05"))
fmt.Println(now.Format("2006/01/02 15:04"))
fmt.Println(now.Format("15:04 2006/01/02"))

2、时间戳与日期字符串相互转化

时间戳转成日期格式,需要先转成将时间戳转成 time.Time 类型再格式化成日期格式。

2.1 根据秒数、纳秒数返回 time.Time 类型

1
2
3
4
now := time.Now()
layout := "2006-01-02 15:04:05"
t := time.Unix(now.Unix(),0) // 参数分别是:秒数,纳秒数
fmt.Println(t.Format(layout))

2.2 根据指定时间返回 time.Time 类型,使用函数 time.Date()

1
2
3
4
5
6
now := time.Now()
layout := "2006-01-02 15:04:05"
//根据指定时间返回 time.Time 类型
//分别指定年,月,日,时,分,秒,纳秒,时区
t := time.Date(2011, time.Month(3), 12, 15, 30, 20, 0, now.Location())
fmt.Println(t.Format(layout))

2.3 日期字符串解析成 time.Time 类型

1
2
3
4
t, _ := time.ParseInLocation("2006-01-02 15:04:05", time.Now().Format("2006-01-02 15:04:05"), time.Local)
fmt.Println(t)
// 输出 2021-01-10 17:28:50 +0800 CST
// time.Local 指定本地时间

解析的时候需要特别注意时区的问题:

1
2
3
4
fmt.Println(time.Now())
fmt.Println(time.Now().Location())
t, _ := time.Parse("2006-01-02 15:04:05", "2021-01-10 15:01:02")
fmt.Println(t)

输出:

1
2
3
2021-01-10 17:22:10.951904 +0800 CST m=+0.000094166
Local
2021-01-10 15:01:02 +0000 UTC

可以看到,time.Now() 使用的 CST(中国标准时间),而 time.Parse() 默认的是 UTC(零时区),它们相差 8 小时。所以解析时常用 time.ParseInLocation(),可以指定时区。img

3、计算、比较日期

讲到日期的计算就不得不提 time 包提供的一种新的类型 Duration,源码是这样定义的:

1
type Duration int64

底层类型是 int64,表示一段时间间隔,单位是 纳秒。

3.1 24小时之内的时间计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
now := time.Now()
fmt.Println(now)
// 1小时1分1s之后
t1, _ := time.ParseDuration("1h1m1s")
fmt.Println(t1)
m1 := now.Add(t1)
fmt.Println(m1)
// 1小时1分1s之前
t2, _ := time.ParseDuration("-1h1m1s")
m2 := now.Add(t2)
fmt.Println(m2)
// 3小时之前
t3, _ := time.ParseDuration("-1h")
m3 := now.Add(t3 * 3)
fmt.Println(m3)
// 10 分钟之后
t4, _ := time.ParseDuration("10m")
m4 := now.Add(t4)
fmt.Println(m4)
// Sub 计算两个时间差
sub1 := now.Sub(m3)
fmt.Println(sub1.Hours()) // 相差小时数
fmt.Println(sub1.Minutes()) // 相差分钟数

额外再介绍两个函数 time.Since()time.Until()

1
2
3
4
5
6
7
8
9
10
11
12
// 返回当前时间与 t 的时间差,返回值是 Duration
time.Since(t Time) Duration
// 返回 t 与当前时间的时间差,返回值是 Duration
time.Until(t Time) Duration

now := time.Now()
fmt.Println(now)
t1, _ := time.ParseDuration("-1h")
m1 := now.Add(t1)
fmt.Println(m1)
fmt.Println(time.Since(m1))
fmt.Println(time.Until(m1))

输出:

1
2
3
4
2021-01-10 20:41:48.668232 +0800 CST m=+0.000095594
2021-01-10 19:41:48.668232 +0800 CST m=-3599.999904406
1h0m0.000199007s
-1h0m0.000203035s

3.2 24小时之外的时间计算

涉及到一天以外的时间计算,就需要用到 time.AddDate(),函数原型:

1
func (t Time) AddDate(years int, months int, days int) Time

比如想知道 一年一个月零一天 之后的时间,就可以这样:

1
2
3
4
now := time.Now()
fmt.Println(now)
m1 := now.AddDate(1,1,1)
fmt.Println(m1)

再比如,想获得 2 天之前时间:

1
2
3
4
now := time.Now()
fmt.Println(now)
m1 := now.AddDate(0,0,-2)
fmt.Println(m1)

3.3 日期比较

日期的比较总共有三种:之前、之后和相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 如果 t 代表的时间点在 u 之前,返回真;否则返回假。
func (t Time) Before(u Time) bool
// 如果 t 代表的时间点在 u 之后,返回真;否则返回假。
func (t Time) After(u Time) bool
// 比较时间是否相等,相等返回真;否则返回假。
func (t Time) Equal(u Time) bool

now := time.Now()
fmt.Println(now)
// 1小时之后
t1, _ := time.ParseDuration("1h")
m1 := now.Add(t1)
fmt.Println(m1)
fmt.Println(m1.After(now))
fmt.Println(now.Before(m1))
fmt.Println(now.Equal(m1))

输出:

1
2
3
4
5
2021-01-10 21:00:44.409785 +0800 CST m=+0.000186800
2021-01-10 22:00:44.409785 +0800 CST m=+3600.000186800
true
true
false

4、常见例子

下面列举一些常见的例子和函数封装。

4.1 日期格式 转 时间戳

1
2
3
4
5
6
7
8
9
10
11
func TimeStr2Time(fmtStr,valueStr, locStr string) int64 {
loc := time.Local
if locStr != "" {
loc, _ = time.LoadLocation(locStr) // 设置时区
}
if fmtStr == "" {
fmtStr = "2006-01-02 15:04:05"
}
t, _ := time.ParseInLocation(fmtStr, valueStr, loc)
return t.Unix()
}

4.2 获取当前时间日期格式

1
2
3
4
5
6
func GetCurrentFormatStr(fmtStr string) string {
if fmtStr == "" {
fmtStr = "2006-01-02 15:04:05"
}
return time.Now().Format(fmtStr)
}

4.3 时间戳 to 日期格式

1
2
3
4
5
6
func Sec2TimeStr(sec int64, fmtStr string) string {
if fmtStr == "" {
fmtStr = "2006-01-02 15:04:05"
}
return time.Unix(sec, 0).Format(fmtStr)
}

Go-regexp正则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"regexp"
)

const text = "My email is ccmouse@gmail.com"

func main() {
compile := regexp.MustCompile(`[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+`)
match := compile.FindString(text)
fmt.Println(match)
}

Go存储基础 — 文件 IO 操作

img

两大 IO 分类

计算的体系架构,CPU,内存,网络,IO。那么 IO 是什么呢?一般理解成 Input、Output 的缩写,通俗话就是输入输出的意思。

IO 分为网络和存储 IO 两种类型(其实网络 IO 和磁盘 IO 在 Go 里面有着根本性区别)。网络 IO 对应的是网络数据传输过程,网络是分布式系统的基石,通过网络把离散的物理节点连接起来,形成一个有机的系统。

存储 IO 对应的就是数据存储到物理介质的过程,通常物理介质对应的是磁盘,磁盘上一般会分个区,然后在上面格式化个文件系统出来,所以普通程序员最常看见的是文件 IO 的形式。

在 Golang 里可以归类出两种读写文件的方式:

  1. 标准库封装:操作对象 File;
  2. 系统调用 :操作对象 fd;

读写数据要素

文件的读写最核心的要素是什么?

通俗来讲:读文件,就是把磁盘上的文件的特定位置的数据读到内存的 buffer 。写文件,就是把内存 buffer 的数据写到磁盘的文件的特定位置

这里注意到两个关键词:

  1. 特定位置;
  2. 内存 buffer;

特定位置怎么理解?怎么指定所谓的特定位置

很简单,用 [ offset, length ] 这两个参数就能标识一段位置。

img

也就是 IO 偏移和长度,Offset 和 Length。

内存 buffer 怎么理解?

归根结底,文件的数据和谁直接打交道?内存,写的时候是从内存写到磁盘文件的,读的时候是从磁盘文件读到内存的。

本质上,下面的 IO 函数都离不开 Offset,Length,buffer 这三个要素。

标准库封装

Go 对文件进行读写非常简单,因为 Go 已经封装了一个非常便捷的使用接口,位于标准库 os 中。Go 标准库对文件 IO 的封装也就是 Go 推荐对文件进行 IO 时使用的操作方式。

打开文件(Open)

1
func OpenFile(name string, flag int, perm FileMode) (*File, error)

Open 文件之后,获取到一个句柄,也就是 File 结构,之后对文件的读写都是基于 File 结构之上进行的。

1
2
3
type File struct {
*file // os specific
}

文件读写只需要针对这个句柄结构体做操作即可。

另外有一点隐藏起来的知识点必须要提一下:偏移。也就是最开始强调的读写 3 要素之一的 Offset 。打开(Open)文件的时候,文件当前偏移量默认设置为 0,也就是说 IO 的起始位置就是文件的最开头。举个例子,如果这个时候,写 4K 的数据到文件,那么就是写 [0, 4K] 这个位置的数据,如果之前这上面已经有数据了,那么就会是覆盖写。

除非 Open 文件的时候指定 O_APPEND 选项,偏移量会设置为文件末尾,那么 IO 都是从文件末尾开始。

文件写操作(Write)

文件 File 句柄对象有两个写方法:

第一种:写一个 buffer 到文件 ,使用文件当前偏移

1
func (f *File) Write(b []byte) (n int, err error)

注意:该写操作会导致文件偏移量的增加。

第二种:从指定文件偏移,写入 buffer 到文件

1
func (f *File) WriteAt(b []byte, off int64) (n int, err error)

注意:该写操作不会更新文件偏移量

文件读操作(Read)

和写对应,文件 File 句柄对象有两个读方法:

第一种:从文件当前偏移读一个 buffer 的数据上来

1
func (f *File) Read(b []byte) (n int, err error)

注意:该读操作会导致文件偏移量的增加。

第二种:从指定文件偏移,读一个 buffer 大小的数据上来

1
func (f *File) ReadAt(b []byte, off int64) (n int, err error)

注意:该读操作不会更新文件偏移量

指定偏移量(Seek)

1
func (f *File) Seek(offset int64, whence int) (ret int64, err error)

这个句柄方法允许用户指定文件的偏移位置。这个很容易理解,举个例子,文件刚开始是 0 字节,写 1M 的数据下去,大小变成 1M,Offset 往后挪 1M ,默认就是往后挪。

现在 Seek 方法允许把写的偏移定位到任意位置,可以就可以从任意地方覆盖写入数据。

所以在 Go 里面,文件 IO 非常简单,先 Open 一个文件,拿到 File 句柄,然后就可以使用这个句柄 Write ,Read,Seek 就能进行 IO 了。

底层的原理

Go 的标准库 os 提供了极其方便的封装,深入最原始的本质可以发现最核心的东西:系统调用

Go 标准库的文件存储 IO 就是基于系统调用之上的。可以稍微跟一下 os.OpenFile 的调用:

os 库的 OpenFile 函数:

1
2
3
4
5
6
7
8
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
f, err := openFileNolog(name, flag, perm)
if err != nil {
return nil, err
}
f.appendMode = flag&O_APPEND != 0
return f, nil
}

稍微看下 openFileNolog 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
var r int
for {
var e error
r, e = syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
if e == nil {
break
}
if runtime.GOOS == "darwin" && e == syscall.EINTR {
continue
}
return nil, &PathError{"open", name, e}
}
return newFile(uintptr(r), name, kindOpenFile), nil
}

可以看到 syscall.Open ,这个函数获取到一个整数,也就是在 c 语言里最常见的 fd 句柄,而 File 结构体则仅仅是基于这个的一层封装而已。

思考下,为什么会有标准库封装这一层存在?

划重点:为了屏蔽操作系统的区别,使用这个标准库的所有操作都是跨平台的。换句话说,如果是特殊操作系统才有的特性,那么在 os 库里就找不到对应封装的 IO 操作。

img

那么怎么使用系统调用?

直接使用 syscall 库,也就是系统调用。从名字也能看出来,系统调用是和操作系统强相关的,因为是操作系统提供的调用接口,所以系统调用会因为操作系统不同而导致不同的特性,不同的接口。

所以,如果直接使用 syscall 库来使用系统调用,那么需要自己来承受系统带来的兼容性问题。

系统调用

系统调用在 syscall 里有一层最基础的封装:

文件 Open

1
func Open(path string, mode int, perm uint32) (fd int, err error) 

文件 Read

1
func Read(fd int, p []byte) (n int, err error) func Pread(fd int, p []byte, offset int64) (n int, err error) 

文件读有两个接口,一个 Read 是从当前默认偏移读一个 buffer 数据,Pread 接口则是从指定位置读数据的接口。

思考一个问题:Pread 从效果上来讲等于 SeekRead 组合起来使用,那么是否可以认为 Pread 就可以被 Seek + Read 替代呢?

不行!根本原因在于 Seek + Read 是在用户层就是两步操作,而 Pread 虽然是 Seek + Read 的效果,但是操作系统给到用户的语义是:Pread 是一个原子操作。还有一个重要区别,Pread 不会改变当前文件的偏移量(普通的 Read 调用会更新偏移量)。

所以,总结下,**Pread** 和顺序调用 **Seek** 后调用 **Read** 有两点重要区别:

  1. Pread 对用户提供的语义是原子操作,在调用 Pread 时,无法中断 SeekRead 操作;
  2. Pread 调用不会更新当前文件偏移量;

文件 Write

1
func Write(fd int, p []byte) (n int, err error) func Pwrite(fd int, p []byte, offset int64) (n int, err error) 

文件写对应也是有两种接口,WrtiePwrite 分别是对应 ReadPread 。同样的,Pwrite 作用上也是相当于先调用 Seek 再调用 Write ,但是同样的也有两点不同

  1. Pwrite 完成 SeekWrite 对外是原子操作的语义;
  2. Pwrite 调用不会更新当前文件偏移量;

文件 Seek

1
func Seek(fd int, offset int64, whence int) (off int64, err error) 

这个函数调用允许用户指定偏移(这个会影响到 ReadWrite 读写的位置)。一般来说,每个打开文件都有一个相关联的“当前文件偏移量”( current file offset )。读(Read)、写(Write)操作都是从当前文件偏移量处开始,并且 ReadWrite 会导致偏移量增加,增加量就是所读写的字节数。

小结一下:Go核心的 Open,Read,Write,Seek 几个系统调用,可以发现一个明显不同与标准 IO 库的区别:系统调用操作对象是一个整数句柄Open 文件得到一个整数 fd,之后的所有 IO 都是针对这个 fd 来操作的。这个明显和标准库不同,os 标准库 OpenFile 得到的是一个 File 结构体,所有的 IO 也是针对这个结构体的。

层次架构

那么究竟封装的层次一般是什么样的呢, Unix 编程里面开篇就有一张如下图:

img

这张图就非常形象的讲明白了整个 Unix 体系结构。

  • 内核是最核心的实现,包括了和 IO 设备,硬件交互等功能。与内核紧密的一层是内核提供给外部调用的系统调用,系统调用提供了用户态到内核态调用的一个通道;

  • 对于系统调用,各个语言的标准库会有一些封装,比如 C 语言的 libc 库,Go 语言的 os ,syscall 库都是类似的地位,这个就是所谓的公共库。这层封装的作用最主要是简化普通程序员使用效率,并且屏蔽系统细节,为跨平台提供基础(同样的,为了跨平台的特性,可能会阉割很多不兼容的功能,所以才会有直接调用系统掉调用的需求);

  • 当然,右上角还看到一个缺口,应用程序除了可以使用公共函数库,其实是可以直接调用系统调用的,但是由此带来的复杂性又应用自己承担。这种需求也是很常见的,标准库封装了通用的东西,同样割舍了很多系统调用的功能,这种情况下,只能通过系统调用来获取;

总结

  1. IO 大类分为网络 IO 和磁盘 IO,IO 对文件来说就是读写操作,写的时候数据从内存到磁盘,读的时候数据从磁盘到内存

  2. Go 文件 IO 最常用的是 os 库,使用 Go 封装的标准库,os.OpenFile 打开,File.WriteFile.Read 进行读写,操作对象都是 File 结构体;

  3. Go 标准库对 IO 的封装是为了屏蔽复杂的系统调用,提供跨平台的使用姿势。然后单独提供 syscall 库,让程序员自我决策使用要使用更丰富的系统调用功能,当然后果自负;

  4. Go 标准库 IO 操作对象是 File ,系统调用 IO 操作对象是 fd(非负整数)。

  5. Open 文件默认当前偏移量是 0 (文件最开始),加上 O_APPEND 参数之后偏移量会是文件末尾。通过 Seek 调用可以任意指定文件偏移,从而影响文件 IO 的位置;

  6. ReadWrite 函数只有 buffer (buffer 有长度),偏移则使用当前文件偏移量;

  7. PreadPwrite 的系统调用效果等同于 Seek 偏移量然后 ReadWrite,但是又大有不同。对外语义是原子操作,并且不更新当前文件偏移量;

Go-文件读写操作

读写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"bufio"
"fmt"
"io"
"os"
)

/*在已存在文件清空原有内容进行追加*/
func main() {
filePath := "D:\\fcofficework\\DNS\\1.txt"
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_APPEND, 0666)
if err != nil {
fmt.Printf("open file err = %v\n", err)
return
}
/*关闭文件流*/
defer file.Close()
/*读取*/
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
break
}
fmt.Print(str)
}
/*写入文件*/
str := "hello FCC您好!!!\r\n"
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
/*因为writer是带缓存的,需要通过flush到磁盘*/
writer.Flush()
}

文件内容拷贝至新文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"io/ioutil"
)

/*将文件1的内容拷贝到文件2*/
func main() {
file1Path := "D:\\fcofficework\\DNS\\1.txt"
file2Path := "D:\\fcofficework\\DNS\\2.txt"
data, err := ioutil.ReadFile(file1Path)
if err != nil {
fmt.Printf("read file err=%v", err)
return
}
err = ioutil.WriteFile(file2Path, data, 0666)
if err != nil {
fmt.Printf("write file err=%v\n", err)
}
}

判断文件或者目录是否存在

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"
"os"
)

/*判断文件以及目录是否存在*/
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
fmt.Println("当前文件存在!")
return true, nil
}
if os.IsNotExist(err) {
fmt.Println("当前文件不存在!")
return false, nil
}
return false, nil
}

func main() {
path := "D:\\fcofficework\\2.txt"
PathExists(path)
}

文件的拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"bufio"
"fmt"
"io"
"os"
)

/*文件的拷贝*/

func CopyFile(dstFileName string, srcFileName string) (written int64, err error) {
srcFile, err := os.Open(srcFileName)
if err != nil {
fmt.Printf("open file err=%v\n", err)
}
reader := bufio.NewReader(srcFile)

dstFile, err := os.OpenFile(dstFileName, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
writer := bufio.NewWriter(dstFile)
defer dstFile.Close()
return io.Copy(writer, reader)
}

func main() {
srcFile := "D:\\Photos\\Datapicture\\mmexport1530688562488.jpg"
dstFile := "D:\\Photos\\1.jpg"
_, err := CopyFile(dstFile, srcFile)
if err == nil {
fmt.Println("拷贝完成!")
} else {
fmt.Println("拷贝失败,err=", err)
}
}

读取文件并统计文件中字符的个数

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
package main

import (
"bufio"
"fmt"
"io"
"os"
)

/*统计文件的字符个数*/

type CharCount struct {
/*英文的个数*/
ChCount int
/*数字的个数*/
NumCount int
/*空格的个数*/
SpaceCount int
/*其他字符的个数*/
OtherCount int
}

func main() {
fileName := "D:\\fcofficework\\DNS\\1.txt"
file, err := os.Open(fileName)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
defer file.Close()
var count CharCount
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
break
}
for _, v := range str {
switch {
case v >= 'a' && v <= 'z':
fallthrough
case v >= 'A' && v <= 'Z':
count.ChCount++
case v == ' ' || v == '\t':
count.SpaceCount++
case v >= '0' && v <= '9':
count.NumCount++
default:
count.OtherCount++
}
}
}
fmt.Printf("字符的个数为:%v 数字的个数为:%v 空格的个数为:%v 其他字符的个数为:%v",
count.ChCount, count.NumCount, count.SpaceCount, count.OtherCount)
}

三种读取文件的方式

通过os读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"os"
)

func main() {
file, err := os.Open("d:\\Photos\\Screenshots\\暗物质\\IMG_20180927_194619.jpg")
if err != nil {
fmt.Println("open file err", err)
}
fmt.Printf("file=%v", file)
err1 := file.Close()
if err1 != nil {
fmt.Println("close file err = ", err1)
}
}

缓冲式读取文件

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
package main

import (
"bufio"
"fmt"
"io"
"os"
)

/*缓冲式读取文件*/
func main() {
file, err := os.Open("d:\\Photos\\Screenshots\\暗物质\\IMG_20180927_194619.jpg")
if err != nil {
fmt.Println("open file err", err)
}
defer file.Close()
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
break
}
fmt.Print(str)
}
fmt.Println("文件读取结束!")
}

通过ioutil读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
"io/ioutil"
)

func main() {
file := "D:\\fcofficework\\DNS\\authorized_keys"
content, err := ioutil.ReadFile(file)
if err != nil {
fmt.Printf("read file err=%v", err)
}
fmt.Printf("%v", string(content))
}

文件写入的案例

在文件写入内容,没有则重新创建

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 (
"bufio"
"fmt"
"os"
)

/*在文件写入内容,没有文件则重新创建*/
func main() {
filePath := "D:\\fcofficework\\DNS\\1.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Printf("open file err = %v\n", err)
return
}
defer file.Close()
str := "hello world\r\n"
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
/*因为writer是带缓存的,需要通过flush到磁盘*/
writer.Flush()
}

在已存在文件清空原有内容重新写入

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 (
"bufio"
"fmt"
"os"
)

/*在已存在文件清空原有内容重新写入*/
func main() {
filePath := "D:\\fcofficework\\DNS\\1.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
fmt.Printf("open file err = %v\n", err)
return
}
defer file.Close()
str := "hello FCC\r\n"
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
/*因为writer是带缓存的,需要通过flush到磁盘*/
writer.Flush()
}

在已存在文件清空原有内容进行追加

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 (
"bufio"
"fmt"
"os"
)

/*在已存在文件清空原有内容进行追加*/
func main() {
filePath := "D:\\fcofficework\\DNS\\1.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
fmt.Printf("open file err = %v\n", err)
return
}
defer file.Close()
str := "hello FCC您好!!!\r\n"
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString(str)
}
/*因为writer是带缓存的,需要通过flush到磁盘*/
writer.Flush()
}

解析命令行参数

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
package main

import (
"fmt"
"os"
)

/*解析命令行参数*/
func main() {
fmt.Println("命令行参数有:", len(os.Args))
for i, v := range os.Args {
fmt.Printf("args[%v]=%v\n", i, v)
}
}
package main

import (
"flag"
"fmt"
)

/*解析命令行参数*/
func main() {
var user string
var pwd string
var host string
var port int
flag.StringVar(&user, "u", "", "用户名,默认为空")
flag.StringVar(&pwd, "pwd", "", "密码,默认为空")
flag.StringVar(&host, "h", "localhost", "主机名,默认为空")
flag.IntVar(&port, "port", 3306, "端口号,默认为空")
/*转换*/
flag.Parse()
fmt.Printf("user=%v pwd=%v host=%v port=%v", user, pwd, host, port)
}

Go-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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package main

import (
"encoding/json"
"fmt"
)

type Monster struct {
Name string `json:"name"`
Age int `json:"age"`
Birthday string `json:"birthday"`
Sal float64 `json:"sal"`
Skill string `json:"skill"`
}

/*结构体序列化*/
func NewMinsterStruct() {
monster := Monster{
Name: "孙悟空",
Age: 500,
Birthday: "2011-11-11",
Sal: 8000.0,
Skill: "如意七十二变",
}
data, err := json.Marshal(&monster)
if err != nil {
fmt.Printf("序列化错误err:%v\n", err)
}
fmt.Printf("Map序列化后=%v\n", string(data))
}

/*Map序列化*/
func MapSerlizer() {
var a map[string]interface{}
a = make(map[string]interface{})
a["name"] = "牛魔王"
a["age"] = 10
a["address"] = "火云洞"
data, err := json.Marshal(a)
if err != nil {
fmt.Printf("序列化错误err:%v\n", err)
}
fmt.Printf("monster序列化后=%v\n", string(data))
}

/*切片序列化*/
func SliceSerlizer() {
var slice []map[string]interface{}
var m1 map[string]interface{}
m1 = make(map[string]interface{})
m1["name"] = "TGH"
m1["age"] = "19"
m1["address"] = "北京"
slice = append(slice, m1)

var m2 map[string]interface{}
m2 = make(map[string]interface{})
m2["name"] = "FCC"
m2["age"] = "18"
m2["address"] = [2]string{"华府", "影视帝国"}
slice = append(slice, m2)

data, err := json.Marshal(slice)
if err != nil {
fmt.Printf("序列化错误err:%v\n", err)
}
fmt.Printf("切片序列化后=%v\n", string(data))
}

/*基本数据类型序列化*/
func FloatSerlize() {
var num1 float64 = 245.56
data, err := json.Marshal(num1)
if err != nil {
fmt.Printf("序列化错误err:%v\n", err)
}
fmt.Printf("基本数据类型序列化后=%v\n", string(data))
}

func main() {
NewMinsterStruct()
MapSerlizer()
SliceSerlizer()
FloatSerlize()
}
Map序列化后={"name":"孙悟空","age":500,"birthday":"2011-11-11","sal":8000,"skill":"如意七十二变"}
monster序列化后={"address":"火云洞","age":10,"name":"牛魔王"}
切片序列化后=[{"address":"北京","age":"19","name":"TGH"},{"address":["华府","影视帝国"],"age":"18","name":"FCC"}]
基本数据类型序列化后=245.56

反序列化

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
package main

import (
"encoding/json"
"fmt"
)

type Monster struct {
Name string `json:"name"`
Age int `json:"age"`
Birthday string `json:"birthday"`
Sal float64 `json:"sal"`
Skill string `json:"skill"`
}

func unmarshalStruct() {
str := "{\"name\":\"孙悟空\",\"age\":500,\"birthday\":\"2011-11-11\",\"sal\":8000,\"skill\":\"如意七十二变\"}"
var monster Monster
err := json.Unmarshal([]byte(str), &monster)
if err != nil {
fmt.Printf("反序列化失败err:%v\n", err)
}
fmt.Printf("反序列化后monster:%v\n", monster)
}

func unmarshallMap() {
str := "{\"address\":\"火云洞\",\"age\":10,\"name\":\"牛魔王\"}"
var a map[string]interface{}
err := json.Unmarshal([]byte(str), &a)
if err != nil {
fmt.Printf("反序列化失败err:%v\n", err)
}
fmt.Printf("反序列化Map后:%v\n", a)
}

func unmarshalSlice() {
str := "[{\"address\":\"北京\",\"age\":\"19\",\"name\":\"TGH\"}," +
"{\"address\":[\"华府\",\"影视帝国\"],\"age\":\"18\",\"name\":\"FCC\"}]"
var slice []map[string]interface{}
err := json.Unmarshal([]byte(str), &slice)
if err != nil {
fmt.Printf("反序列化失败err:%v\n", err)
}
fmt.Printf("反序列化Slice后:%v\n", slice)
}

func main() {
unmarshalStruct()
unmarshallMap()
unmarshalSlice()
}

输出结果:

1
2
3
反序列化后monster:{孙悟空 500 2011-11-11 8000 如意七十二变} 
反序列化Map后:map[address:火云洞 age:10 name:牛魔王]
反序列化Slice后:[map[address:北京 age:19 name:TGH] map[address:[华府 影视帝国] age:18 name:FCC]]

Go-HTTP包的使用

Web是基于http协议的一个服务,Go语言里面提供了一个完善的net/http包,通过http包可以很方便的搭建起来一个可以运行的Web服务。同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作。

http包建立Web服务器

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
package main
import (
"fmt"
"net/http"
"strings"
"log"
)
func sayhelloName(w http.ResponseWriter, r *http.Request) {
r.ParseForm() //解析参数,默认是不会解析的
fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息
fmt.Println("path", r.URL.Path)
fmt.Println("scheme", r.URL.Scheme)
fmt.Println(r.Form["url_long"])
for k, v := range r.Form {
fmt.Println("key:", k)
fmt.Println("val:", strings.Join(v, ""))
}
fmt.Fprintf(w, "Hello golang!") //这个写入到w的是输出到客户端的
}
func main() {
http.HandleFunc("/", sayhelloName) //设置访问的路由
err := http.ListenAndServe(":8080", nil) //设置监听的端口
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

上面这个代码,build之后,然后执行web.exe,这个时候其实已经在8080端口监听http链接请求了。

在浏览器输入http://localhost:8080

可以看到浏览器页面输出了Hello golang!

浏览器输入地址:

1
http://localhost:8080/?url_long=var1&url_long=var2

可以看看浏览器输出的是什么

看到上面的代码,要编写一个Web服务器很简单,只要调用http包的两个函数就可以了。

使用http包请求页面

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
package main

import (
"fmt"
"net/http"
"net/http/httputil"
)

func main() {
request, err := http.NewRequest(http.MethodGet, "http://www.imooc.com", nil)
if err != nil {
panic(err)
}
request.Header.Add("User-Agent",
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1")

client := http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
fmt.Println("Redirect:", req)
return nil
},
}

resp, err := client.Do(request)
//resp, err := http.DefaultClient.Do(request)
//resp, err := http.Get("http://www.imooc.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
s, err := httputil.DumpResponse(resp, true)
if err != nil {
panic(err)
}
fmt.Println(string(s))
}

程序运行打印出HTML内容

img

net/http包的坑——i/o timeout

问题

来看一段日常代码。

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
63
64
65
66
67
68
package main

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"time"
)

var tr *http.Transport

func init() {
tr = &http.Transport{
MaxIdleConns: 100,
Dial: func(netw, addr string) (net.Conn, error) {
conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
if err != nil {
return nil, err
}
err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接受数据超时
if err != nil {
return nil, err
}
return conn, nil
},
}
}

func main() {
for {
_, err := Get("http://www.baidu.com/")
if err != nil {
fmt.Println(err)
break
}
}
}


func Get(url string) ([]byte, error) {
m := make(map[string]interface{})
data, err := json.Marshal(m)
if err != nil {
return nil, err
}
body := bytes.NewReader(data)
req, _ := http.NewRequest("Get", url, body)
req.Header.Add("content-type", "application/json")

client := &http.Client{
Transport: tr,
}
res, err := client.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
resBody, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
return resBody, nil
}

做的事情,比较简单,就是循环去请求http://www.baidu.com/, 然后等待响应。

看上去貌似没啥问题吧。

代码跑起来,也确实能正常收发消息。

但是这段代码跑一段时间,就会出现i/o timeout的报错。

这其实是最近排查了的一个问题,发现这个坑可能比较容易踩上,这边对代码做了简化。

实际生产中发生的现象是,golang服务在发起http调用时,虽然http.Transport设置了3s超时,会偶发出现i/o timeout的报错。

但是查看下游服务的时候,发现下游服务其实100ms就已经返回了。

排查

img

五层网络协议对应的消息体变化分析

就很奇怪了,明明服务端显示处理耗时才100ms,且客户端超时设的是3s, 怎么就出现超时报错i/o timeout呢?

这里推测有两个可能。

  • 因为服务端打印的日志其实只是服务端应用层打印的日志。但客户端应用层发出数据后,中间还经过客户端的传输层,网络层,数据链路层和物理层,再经过服务端的物理层,数据链路层,网络层,传输层到服务端的应用层。服务端应用层处耗时100ms,再原路返回。那剩下的3s-100ms可能是耗在了整个流程里的各个层上。比如网络不好的情况下,传输层TCP使劲丢包重传之类的原因。
  • 网络没问题,客户端到服务端链路整个收发流程大概耗时就是100ms左右。客户端处理逻辑问题导致超时。

一般遇到问题,大部分情况下都不会是底层网络的问题,大胆怀疑是自己的问题就对了,不死心就抓个包看下。

img

抓包结果

分析下,从刚开始三次握手(画了红框的地方)。

到最后出现超时报错i/o timeout(画了蓝框的地方)。

从time那一列从7到10,确实间隔3s。而且看右下角的蓝框,是51169端口发到80端口的一次Reset连接。

80端口是服务端的端口。换句话说就是客户端3s超时主动断开链接的。

但是再仔细看下第一行三次握手到最后客户端超时主动断开连接的中间,其实有非常多次HTTP请求。

回去看代码设置超时的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tr = &http.Transport{
MaxIdleConns: 100,
Dial: func(netw, addr string) (net.Conn, error) {
conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
if err != nil {
return nil, err
}
err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接受数据超时
if err != nil {
return nil, err
}
return conn, nil
},
}

也就是说,这里的3s超时,其实是在建立连接之后开始算的,而不是单次调用开始算的超时。

看注释里写的是

SetDeadline sets the read and write deadlines associated with theconnection.

超时原因

大家知道HTTP是应用层协议,传输层用的是TCP协议。

HTTP协议从1.0以前,默认用的是短连接,每次发起请求都会建立TCP连接。收发数据。然后断开连接。

TCP连接每次都是三次握手。每次断开都要四次挥手。

其实没必要每次都建立新连接,建立的连接不断开就好了,每次发送数据都复用就好了。

于是乎,HTTP协议从1.1之后就默认使用长连接。具体相关信息可以看之前的这篇文章

那么golang标准库里也兼容这种实现。

通过建立一个连接池,针对每个域名建立一个TCP长连接,比如http://baidu.com和http://golang.com就是两个不同的域名。

第一次访问http://baidu.com域名的时候会建立一个连接,用完之后放到空闲连接池里,下次再要访问http://baidu.com的时候会重新从连接池里把这个连接捞出来复用。

img

复用长连接

为什么要强调是同一个域名:一个域名会建立一个连接,一个连接对应一个读goroutine和一个写goroutine。正因为是同一个域名,所以最后才会泄漏3个goroutine,如果不同域名的话,那就会泄漏1+2*N个协程,N就是域名数。

假设第一次请求要100ms,每次请求完http://baidu.com后都放入连接池中,下次继续复用,重复29次,耗时2900ms。

第30次请求的时候,连接从建立开始到服务返回前就已经用了3000ms,刚好到设置的3s超时阈值,那么此时客户端就会报超时i/o timeout。

虽然这时候服务端其实才花了100ms,但耐不住前面29次加起来的耗时已经很长。

也就是说只要通过http.Transport设置了err = conn.SetDeadline(time.Now().Add(time.Second * 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)

var tr *http.Transport

func init() {
tr = &http.Transport{
MaxIdleConns: 100,
// 下面的代码被干掉了
//Dial: func(netw, addr string) (net.Conn, error) {
// conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
// if err != nil {
// return nil, err
// }
// err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接受数据超时
// if err != nil {
// return nil, err
// }
// return conn, nil
//},
}
}


func Get(url string) ([]byte, error) {
m := make(map[string]interface{})
data, err := json.Marshal(m)
if err != nil {
return nil, err
}
body := bytes.NewReader(data)
req, _ := http.NewRequest("Get", url, body)
req.Header.Add("content-type", "application/json")

client := &http.Client{
Transport: tr,
Timeout: 3*time.Second, // 超时加在这里,是每次调用的超时
}
res, err := client.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
resBody, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
return resBody, nil
}

func main() {
for {
_, err := Get("http://www.baidu.com/")
if err != nil {
fmt.Println(err)
break
}
}
}

看注释会发现,改动的点有两个

  • http.Transport里的建立连接时的一些超时设置干掉了。
  • 在发起http请求的时候会场景http.Client,此时加入超时设置,这里的超时就可以理解为单次请求的超时了。同样可以看下注释
1
Timeout specifies a time limit forrequestsmade by this Client.

到这里,代码就改好了,实际生产中问题也就解决了。

实例代码里,如果拿去跑的话,其实还会下面的错

1
Get http://www.baidu.com/: EOF

这个是因为调用得太猛了,http://www.baidu.com那边主动断开的连接,可以理解为一个限流措施,目的是为了保护服务器,毕竟每个人都像这么搞,服务器是会炸的。。。

解决方案很简单,每次HTTP调用中间加个sleep间隔时间就好。

到这里,其实问题已经解决了,下面会在源码层面分析出现问题的原因。

源码分析

用的go版本是1.12.7。

从发起一个网络请求开始跟。

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
res, err := client.Do(req)
func (c *Client) Do(req *Request) (*Response, error) {
return c.do(req)
}

func (c *Client) do(req *Request) {
// ...
if resp, didTimeout, err = c.send(req, deadline); err != nil {
// ...
}
// ...
}
func send(ireq *Request, rt RoundTripper, deadline time.Time) {
// ...
resp, err = rt.RoundTrip(req)
// ...
}

// 从这里进入 RoundTrip 逻辑
/src/net/http/roundtrip.go: 16
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
return t.roundTrip(req)
}

func (t *Transport) roundTrip(req *Request) (*Response, error) {
// 尝试去获取一个空闲连接,用于发起 http 连接
pconn, err := t.getConn(treq, cm)
// ...
}

// 重点关注这个函数,返回是一个长连接
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistConn, error) {
// 省略了大量逻辑,只关注下面两点
// 有空闲连接就返回
pc := <-t.getIdleConnCh(cm)

// 没有创建连接
pc, err := t.dialConn(ctx, cm)

}

这里上面很多代码,其实只是为了展示这部分代码是怎么跟踪下来的,方便大家去看源码的时候去跟一下。

最后一个上面的代码里有个getConn方法。在发起网络请求的时候,会先取一个网络连接,取连接有两个来源。

  • 如果有空闲连接,就拿空闲连接
1
2
3
4
5
6
7
// /src/net/http/tansport.go:810
func (t *Transport) getIdleConnCh(cm connectMethod) chan *persistConn {
// 返回放空闲连接的chan
ch, ok := t.idleConnCh[key]
// ...
return ch
}
  • 没有空闲连接,就创建长连接。
1
2
3
4
5
6
7
8
9
// /src/net/http/tansport.go:1357
func (t *Transport) dialConn() {
//...
conn, err := t.dial(ctx, "tcp", cm.addr())
// ...
go pconn.readLoop()
go pconn.writeLoop()
// ...
}

当第一次发起一个http请求时,这时候肯定没有空闲连接,会建立一个新连接。同时会创建一个读goroutine和一个写goroutine。

img

读写协程

注意上面代码里的t.dial(ctx, "tcp", cm.addr()),如果像文章开头那样设置了http.Transport

1
2
3
4
5
6
7
8
9
10
11
Dial: func(netw, addr string) (net.Conn, error) {
conn, err := net.DialTimeout(netw, addr, time.Second*2) //设置建立连接超时
if err != nil {
return nil, err
}
err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //设置发送接受数据超时
if err != nil {
return nil, err
}
return conn, nil
},

那么这里就会在下面的dial里被执行到

1
2
3
4
5
func (t *Transport) dial(ctx context.Context, network, addr string) (net.Conn, error) {
// ...
c, err := t.Dial(network, addr)
// ...
}

这里面调用的设置超时,会执行到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// /src/net/net.go
func (c *conn) SetDeadline(t time.Time) error {
//...
c.fd.SetDeadline(t)
//...
}

//...

func setDeadlineImpl(fd *FD, t time.Time, mode int) error {
// ...
runtime_pollSetDeadline(fd.pd.runtimeCtx, d, mode)
return nil
}


//go:linkname poll_runtime_pollSetDeadline internal/poll.runtime_pollSetDeadline
func poll_runtime_pollSetDeadline(pd *pollDesc, d int64, mode int) {
// ...
// 设置一个定时器事件
rtf = netpollDeadline
// 并将事件注册到定时器里
modtimer(&pd.rt, pd.rd, 0, rtf, pd, pd.rseq)
}

上面的源码,简单来说就是,当第一次调用请求的,会建立个连接,这时候还会注册一个定时器事件,假设时间设了3s,那么这个事件会在3s后发生,然后执行注册事件的逻辑。而这个注册事件就是netpollDeadline。注意这个netpollDeadline,待会会提到。

img

读写协程定时器事件

设置了超时事件,且超时事件是3s后之后,发生。再次期间正常收发数据。一切如常。

直到3s过后,这时候看读goroutine,会等待网络数据返回。

1
2
3
4
5
6
7
// /src/net/http/tansport.go:1642
func (pc *persistConn) readLoop() {
//...
for alive {
_, err := pc.br.Peek(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
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
src/bufio/bufio.go: 129
func (b *Reader) Peek(n int) ([]byte, error) {
// ...
b.fill()
// ...
}

func (b *Reader) fill() {
// ...
n, err := b.rd.Read(b.buf[b.w:])
// ...
}

/src/net/http/transport.go: 1517
func (pc *persistConn) Read(p []byte) (n int, err error) {
// ...
n, err = pc.conn.Read(p)
// ...
}

// /src/net/net.go: 173
func (c *conn) Read(b []byte) (int, error) {
// ...
n, err := c.fd.Read(b)
// ...
}

func (fd *netFD) Read(p []byte) (n int, err error) {
n, err = fd.pfd.Read(p)
// ...
}

/src/internal/poll/fd_unix.go:
func (fd *FD) Read(p []byte) (int, error) {
//...
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
// ...
}

func (pd *pollDesc) waitRead(isFile bool) error {
return pd.wait('r', isFile)
}

func (pd *pollDesc) wait(mode int, isFile bool) error {
// ...
res := runtime_pollWait(pd.runtimeCtx, mode)
return convertErr(res, isFile)
}

直到跟到runtime_pollWait,这个可以简单认为是等待服务端数据返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
//go:linkname poll_runtime_pollWait internal/poll.runtime_pollWait
func poll_runtime_pollWait(pd *pollDesc, mode int) int {

// 1.如果网络正常返回数据就跳出
for !netpollblock(pd, int32(mode), false) {
// 2.如果有出错情况也跳出
err = netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}
}
return 0
}

整条链路跟下来,就是会一直等待数据,等待的结果只有两个

  • 有可以读的数据
  • 出现报错

这里面的报错,又有那么两种

  • 连接关闭
  • 超时
1
2
3
4
5
6
7
8
9
func netpollcheckerr(pd *pollDesc, mode int32) int {
if pd.closing {
return 1 // errClosing
}
if (mode == 'r' && pd.rd < 0) || (mode == 'w' && pd.wd < 0) {
return 2 // errTimeout
}
return 0
}

其中提到的超时,就是指这里面返回的数字2,会通过下面的函数,转化为ErrTimeout, 而ErrTimeout.Error()其实就是i/o timeout。

1
2
3
4
5
6
7
8
9
10
11
12
func convertErr(res int, isFile bool) error {
switch res {
case 0:
return nil
case 1:
return errClosing(isFile)
case 2:
return ErrTimeout // ErrTimeout.Error() 就是 "i/o timeout"
}
println("unreachable: ", res)
panic("unreachable")
}

那么问题来了。上面返回的超时错误,也就是返回2的时候的条件是怎么满足的?

1
2
3
if (mode == 'r' && pd.rd < 0) || (mode == 'w' && pd.wd < 0) {
return 2 // errTimeout
}

还记得刚刚提到的netpollDeadline吗?

这里面放了定时器3s到点时执行的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func timerproc(tb *timersBucket) {
// 计时器到设定时间点了,触发之前注册函数
f(arg, seq) // 之前注册的是 netpollDeadline
}

func netpollDeadline(arg interface{}, seq uintptr) {
netpolldeadlineimpl(arg.(*pollDesc), seq, true, true)
}

/src/runtime/netpoll.go: 428
func netpolldeadlineimpl(pd *pollDesc, seq uintptr, read, write bool) {
//...
if read {
pd.rd = -1
rg = netpollunblock(pd, 'r', false)
}
//...
}

这里会设置pd.rd=-1,是指poller descriptor.read deadline,含义网络轮询器文件描述符的读超时时间, 在linux里万物皆文件,这里的文件其实是指这次网络通讯中使用到的socket。

这时候再回去看发生超时的条件就是if (mode == 'r' && pd.rd < 0)

至此。代码里就收到了io timeout的报错。

总结

  • 不要在http.Transport中设置超时,那是连接的超时,不是请求的超时。否则可能会出现莫名io timeout报错。
  • 请求的超时在创建client里设置。

Go中Unicode相关包

Go中Unicode相关包

Go 语言让复杂的编码问题变得简单很多,极大的减轻了程序员的心智负担。为了方便对 unicode 字符串进行处理,Go 语言标准库提供三个包:unicode、unicode/utf8 和 unicode/utf16。

这里简单介绍下三个包的功能:

  • unicode:unicode 提供数据和函数来测试 Unicode 代码点(Code Point,用 rune 存储)的某些属性。

  • unicode/utf8:用于处理 UTF-8 编码的文本,提供一些常量和函数,包括在 rune(码点) 和 UTF-8 字节序列之间的转换。

  • unicode/utf16:函数比较少,主要是 UTF-16 序列的编码和解码。

Go 中字符串的写法。

在 Go 语言中,字符串字面值有 4 种写法,比如「徐新华」可以这么写:

1
2
3
4
s1 := "徐新华"
s2 := "\u5F90\u65B0\u534E"
s3 := "\U00005F90\U000065B0\U0000534E"
s4 := "\xe5\xbe\x90\xe6\x96\xb0\xe5\x8d\x8e"

简单来生活就是 \u 紧跟四个十六进制数,\U 紧跟八个十六进制数。其中 \u 或 \U 代表后面是 Unicode 码点。而 \x 紧跟两个十六进制数,这些十六进制不是 Unicode 码点,而是 UTF-8 编码。

下面的代码有利于理解:

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"
"unicode/utf8"
)
func main() {
s := `徐新华`
var (
buf = make([]byte, 4)
n int
)
fmt.Println("字符\tUnicode码点\tUTF-8编码十六进制\tUTF-8编码二进制")
for _, r := range s {
n = utf8.EncodeRune(buf, r)
fmt.Printf("%q\t%U\t\t%X\t\t%b\n", r, r, buf[:n], buf[:n])
}

s2 := "\u5F90\u65B0\u534E"
s3 := "\U00005F90\U000065B0\U0000534E"
s4 := "\xe5\xbe\x90\xe6\x96\xb0\xe5\x8d\x8e"

fmt.Println(s2)
fmt.Println(s3)
fmt.Println(s4)
}

运行结果:

1
2
3
4
5
6
7
字符 Unicode码点 UTF-8编码十六进制 UTF-8编码二进制
'徐' U+5F90 E5BE90 [11100101 10111110 10010000]
'新' U+65B0 E696B0 [11100110 10010110 10110000]
'华' U+534E E58D8E [11100101 10001101 10001110]
徐新华
徐新华
徐新华

此外,关于字符串其他方面的处理,比如编码转换等,可以到 https://pkg.go.dev/golang.org/x/text 里找。

大小端问题(Little endian 和 Big endian)

一个字符使用多字节存储时,涉及到哪个在前哪个在后。以汉字「徐」为例,Unicode 码点是 5F90,需要用两个字节存储,一个字节是5F,另一个字节是90。存储的时候,5F在前,90 在后,这就是 Big endian 方式;90在前,5F在后,这是 Little endian 方式。

这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-endian)敲开还是从小头(Little-endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。

第一个字节在前,就是”大端方式”(Big endian),第二个字节在前就是”小端方式”(Little endian)。

那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?

Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格”(zero width no-break space),用 FEFF 表示。这正好是两个字节,而且 FF 比 FE 大1。

如果一个文本文件的头两个字节是 FE FF,就表示该文件采用大端方式;如果头两个字节是 FF FE,就表示该文件采用小端方式。

但从上面关于 UTF-8 编码的说明可以看出,虽然 UTF-8 存在多字节表示一个字符的情况,但顺序是固定的,没有字节序的问题。Unix 系统下,UTF-8 没有任何前置字符,但 Windows 下记事本保存的 UTF-8 文件会带上 BOM(Byte Order Mark),即 EF BB BF 这三个字节。关于这一点,Unicode 之父 Rob Pike 明确说 UTF-8 不需要 BOM,所以一开始 Go 源文件是不允许有 BOM 的,否则编译不通过,不过现在已经可以有了。但建议还是别带 BOM。

UTF-8 带 BOM 说不是为了区分字节序,而是为了更方便的知晓这是一个 UTF-8 文件。

Go 标准库之 unsafe 包

1. unsafe包

1.1. ArbitraryType

unsafe包下定义了一个ArbitratyType类型,代表了任意的Go表达式。

1
type ArbitraryType int

1.2. Pointer

Pointer定义:

1
type Pointer *ArbitraryType

Pointer代表了一个指向任意类型的指针,有四种只适用对Pointer而不适用于其他类型的操作。

  • 任意类型的指针值可以被转换为一个Pointer

  • 一个Pointer可以被转换为任意类型的指针值

  • 一个uintptr可以被转换为一个Pointer

  • 一个Pointer也可以被转换为一个uintptr

因此,Pointer可以跳过类型系统而直接指向任意类型。所以需要十分小心的使用。

关于使用Pointer的规则,不使用这些规则的代码是不可用的,或者在未来是不可用的。

1.2.1. 使用Pointer作为中间者将*T1转换为*T2

前提是T2的大小不超过T1,而且两者的内存分布相同。

1
func Float64bits(f float64) uint64 {  return *(*uint64)(unsafe.Pointer(&f))}

1.2.2. 把Pointer转换为uintptr

Pointer转换为uintptr将产生一个指向类型值的int变量。常用来打印一个uintptr

uintptr转换为Pointer是不可用的。

因为uintptr是一个整数值,而不是引用。就是说uintptr和指针没有任何关系。可以说是将Pointer指向的地址的值返回给uintptr,即使uintptr中的值对应的地址的对象更新了或者删除了,uintptr也不会改变。

1.2.3. 把Pointer转为uintptr再转换回Pointer,其中带有uintptr数值运算

如果Pointer指向一个分配的对象,那么如下转换可以把Pointer指针向后移动。

1
p = unsafe.Pointer(uintptr(p) + offset)

最常用的是指向结构体中不同字段或者数组中的元素

1
2
3
4
// equivalent to f := unsafe.Pointer(&s.f)
f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))
// equivalent to e := unsafe.Pointer(&x[i])
e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))

这可以用来向前或向后移动指针,通过加或者减offset。指针移动之后,也应该指向该内存范围中。

Pointer移动超过其对象的原始内存分配范围是不可用的,如:

1
2
3
4
5
6
7
// INVALID: end points outside allocated space.
var s thing
end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))

// INVALID: end points outside allocated space.
b := make([]byte, n)
end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))

当然如下代码也是错误的,因为uintptr不可以储存在变量中:

1
2
3
4
5
6
7
8
// INVALID: uintptr cannot be stored in variable
// before conversion back to Pointer.
u := uintptr(p)
p = unsafe.Pointer(u + offset)
Pointer`必须指向一个已经分配好的对象,而不能是`nil
// INVALID: conversion of nil pointer
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + offset)

1.2.4. 当调用syscall.Syscall时,需要把Poiner转换为uintptr

syscall包下的Syscall函数把uintptr参数传递给操作系统,然后根据调用的相关信息,把相应的uintptr再转换为指针。

如果一个指针参数必须被转换为uintptr作为参数的话,这个转换只能在调用函数中的参数表达式完成,因为uintptr是不能储存在变量中的。

1
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))

编译器处理函数调用中的指针时,该指针所指向的对象会被保留到函数调用结束,即使该对象在函数调用时并不使用。

如下是错误的代码,因为uintptr不能保存在变量中

1
2
3
4
// INVALID: uintptr cannot be stored in variable
// before implicit conversion back to Pointer during system call.
u := uintptr(unsafe.Pointer(p))
syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n))

1.2.5. 将reflect.Value.Pointer或者reflect.Value.UnsafeAddr的结果从uintptr转换为Pointer

reflectValuePointer方法和UnsafeAddr方法返回的是uintptr而不是Pointer类型,以便于调用者不使用usafe包就可以转换为任意类型。这也意味着,这两个方法的返回值必须使用Pointer进行转换才可以使用:

1
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))

因为这两个函数调用的返回值是uintptr,所以也是不可以变量储存的。

1.2.6. reflect.SliceHeader或者reflect.StringHeaderData字段同Pointer的相互转换

前面说过,返回uintptr是为了调用者可以直接进行不同类型的转换,而不用导入unsafe包。这意味着,只有当指针解析为切片或者字符串时SliceHeaderStringHeader才可以被使用。

1
2
3
4
var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p)) // case 6 (this case)
hdr.Len = n

通常情况下,SliceHeaderStringHeader只能作为*SliceHeader*StringHeader使用,而不可以使用其结构体形式。

1
2
3
4
5
// INVALID: a directly-declared header will not hold Data as a reference.
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n
s := *(*string)(unsafe.Pointer(&hdr)) // p possibly already lost

1.3 Sizeof函数

定义:

1
func Sizeof(x ArbitraryType) uintptr

直接复制标准文档中的内容,下同。

Sizeof返回类型v本身数据所占用的字节数。返回值是“顶层”的数据占有的字节数。例如,若v是一个切片,它会返回该切片描述符的大小,而非该切片底层引用的内存的大小。

1.4 Alignof

定义:

1
func Alignof(v ArbitraryType) uintptr

Alignof返回类型v的对齐方式(即类型v在内存中占用的字节数);若是结构体类型的字段的形式,它会返回字段f在该结构体中的对齐方式。

1.5 Offsetof

定义:

1
func Offsetof(v ArbitraryType) uintptr

Offsetof返回类型v所代表的结构体字段在结构体中的偏移量,它必须为结构体类型的字段的形式。换句话说,它返回该结构起始处与该字段起始处之间的字节数。

总结

1.2中的Pointeruintptr的区别:

假设在内存中有一个变量a := 1

那么p := Pointer(&a)中,p包含的就是a的实际地址,假设为1000,当a在内存中移动时,p中的地址值也会实时更新。

uintprt(p)只是1000,就是a的地址值,但是当a在内存中移动时,原来获取的uintptr值并不会发生变化,一直都是1000。

也是因为这个原因,syscall.Syscall传入的uintptr如果代表一个对象的指针,那么该对象在内存中是一直被保留的,而且不能移动,否则的话uintptr指向的就不是原来的对象了,容易内存泄漏。

还有一个就是uintptr不能保存在变量中,只能使用Pointer进行转换然后才能保存。

编码分析

背景

HTTP 协议基于文本传输,字符编码将文本变为二进制,二进制编码将二进制变为文本。TCP 协议基于二进制传输,数据读取时需要处理字节序。本文将介绍常见的字符编码、二进制编码及字节序,并一探 Golang 中的实现。

字符编码

引言:如何把“Hello world”变成字节?

  • Step1:得到要表示的全量字符(字符表)

  • Step2:为每个字符指定一个整数编号(编码字符集)

  • Step3:将编号映射成有限长度比特值(字符编码表)

字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。全世界共使用 5651 种语言,其中使用人数超过 5000 万的语言有 13 种,每种语言有自己的字符。汉语中,一个汉字就是一个字符。英语中,一个字母就是一个字符。甚至看不见的也可以是字符(如控制字符)。字符的集合即为字符表,如英文字母表,阿拉伯数字表。ASCII 码表中一共有 128 个字符。

编码字符集(CCS:Coded Character Set)

为字符表中的每个字符指定一个编号(码点,Code Point),即得到编码字符集。常见有 ASCII 字符集、Unicode 字符集、GB2312 字符集、BIG5 字符集、 GB18030 字符集等。ASCII 字符集中一共有 128 个字符,包括了 94 个可打印字符(英文大小写字母 52 个、阿拉伯数字 10 个、西文符号 32 个)和 34 个控制符或通信专用字符,码点值范围为[0, 128),如下图所示。Unicode 字符集是一个很大的集合,现有容量将近 2^21 个字符,码点值范围为[0, 2^20+2^16)。

img

ASCII字符编码表

字符编码表(CEF:Character Encoding Form)

编码字符集只定义了字符与码点的映射,并没有规定码点的字节表示方式。由于 1 个字节可以表示 256 个编号,足以容纳 ASCII 字符集,因此ASCII 编码的规则很简单:直接将码点值用 uint8 表示即可。对于 Unicode 字符集,容纳 2^21 至少需要 3 字节。可以采用类似 ASCII 的编码规则:直接将编码点值用 uint32 表示即可,这正是 UTF-32 编码

这种一刀切的定长编码方式虽然简单粗暴,弊端也很明显:对于纯英文文本,UTF-32 编码空间占用将是 ACSII 编码的 4 倍,造成极大的空间浪费,几乎没什么人用。有没有更优雅的解决方案?当然,这就是 UTF-8 和 UTF-16,两种当前比较流行的 Unicode 编码方式。

UTF-8

历史的经验,成功的设计往往具有包容性。UTF-8 是一个典型,漂亮的实现了对 ASCII 码的向后兼容,以保证可以被大众接受。UTF-8 是目前互联网上使用最广泛的一种 Unicode 编码方式,它的最大特点就是可变长,随码点变换长度(从 1 字节到 4 字节)。text

img

大道至简,优雅的设计一定是简单的,UTF-8 的编码规则也诠释了这一点。编码规则如下:

  1. <=127(U+7F)的码点采用单字节编码,与 ASCII 保持一致;
  2. >127(U+7F)的码点采用 N 字节(N 属于 2,3,4)编码,首字节的前 N 位为 1,第 N+1 位为 0,剩余 N-1 个字节的前两位都为 10,剩下的二进制位使用字符的码点来填充。

其中(U+7F)表示 Unicode 的十六进制码点值,即 127。如果觉得编码规则抽象,结合下表更加清晰:

Unicode 码点范围 码点数量 UTF-8 编码格式
0000 0000 ~ 0000 007F 2^7 0xxxxxxx
0000 0080 ~ 0000 07FF 2^11 - 2^7 110xxxxx 10xxxxxx
0000 0800 ~ 0000 FFFF 2^16 - 2^11 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 ~ 0010 FFFF 2^20 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

举个例子,如“汉”的 Unicode 码点是 U+6C49(110 1100 0100 1001),根据上表可得需要 3 字节编码,填充码点值后得到 0xE6 0xB7 0x89(11100110 10110001 10001001)。

根据编码规则,解码也很简单,关键是如何判断连续的字节数:首字节连续 1 的个数即为字节数

需要一提的是,在 MySQL 中,utf8 是“虚假的 utf8”,最大只支持 3 个字节,如果建表时选择 CHARSET=utf8,会导致很多特殊字符和 emoji 表情都无法插入。utf8mb4 才是“真正的 utf8”,mb4 即most bytes 4。为什么 MySQL 中 utf8 最大只支持 3 字节?历史原因,在 MySQL 刚开发那会儿,Unicode 空间只有 2^16,Unicode 委员会还在做 “65535 个字符足够全世界用了”的美梦呢。

UTF-16

在 C/C++ 中遇到的wchar_t类型或 Java 中的char类型,这些类型占内存两个字节,因为 Unicode 中常用的字符都处于[U+0, U+FFFF](基本平面)的范围之内,因此两个字节几乎可以覆盖大部分的常用字符,这正是 UTF-16 编码的一个前提。

相比 UTF-32 与 UTF-8,UTF-16 编码是一个折中:小于(U+FFFF)2^16 的码点(基本平面)使用 2 字节编码,大于(U+FFFF)2^16 的码点(辅助码点)使用 4 字节编码。由于基础平面空间会占用 2 字节的所有比特位,无法像 UTF-8 那样留有“10”前缀。那么问题来了:当遇到两个节时,如何判断是 2 字节编码还是 4 字节编码?

UTF-16 的编码的另一个前提:在基本平面内,**[U+D800, U+DFFF]**是一个空段(空间大小为 2^11),这些码点不对应任何字符。因此,这个空段可以用来映射辅助平面的字符。

辅助平面容量为 2^20,至少需要 20 个二进制位,UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 到 U+DBFF(空间大小 2^10),称为高位(H),后 10 位映射在 U+DC00 到 U+DFFF(空间大小 2^10),称为低位(L)。

映射方式采用线性映射。Unicode3.0 中给出了辅助平面字符的转换公式:

H = Math.floor((c-0x10000) / 0x400) + 0xD800

L = (c - 0x10000) % 0x400 + 0xDC00

也就是说,一个辅助平面的码点,被拆成两个基本平面的空段码点表示。如果双字节的值在[U+D800, U+DBFF]中,则要和后续相邻的双字节一同解码。具体编码规则为:

  1. <= (U+FFFF)的码点采用双字节编码,直接将码点使用 uint16 表示;
  2. > (U+FFFF)的码点采用 4 字节编码,作差计算码点溢出值,将溢出值用 uint20 表示后,前 10 位映射到[U+D800, U+DBFF],后 10 位映射到[U+DC00, U+DFFF];

小结: 定长编码的优点是转换规则简单直观,查找效率高,缺点是空间浪费,以及不可扩展。如果 Unicode 字符集进一步扩充,UTF-16 和 UTF-32 都将不可用,而 UTF-8 具有更强的可扩展性。

Golang 中字符编码

不像 C++、Java 等语言支持五花八门的字符编码,Golang 遵从“大道至简”的原则:全用 UTF-8。所以 go 程序员再也不用担心乱码问题,甚至可以用汉字和表情包写代码,string 与字节数组转换也是直接转换。

1
2
3
4
5
6
7
8
9
10
func TestTemp(t *testing.T) {
来自打工人的问候()
}

func 来自打工人的问候() {
问候语 := "早安,打工人😁"
fmt.Println(问候语)
bytes := []byte(问候语)
fmt.Println(hex.EncodeToString(bytes))
}

// 执行结果–>

1
2
早安,打工人😁
e697a9e5ae89efbc8ce68993e5b7a5e4babaf09f9881

值得一提的是,Golang 中 string 的底层模型就是字节数组,所以类型转换过程中无需编解码。也因此,Golang 中 string 的底层模型是字节数组,其长度并非字符数,而是对应字节数。如果要取字符数,需要先将字符串转换为字符数组。字符类型(rune)实际上是 int32 的别名,即用 UTF-32 编码表示字符

1
2
3
4
5
6
7
8
func TestTemp(t *testing.T) {
fmt.Println(len("早")) // 3
fmt.Println(len([]byte("早"))) // 3
fmt.Println(len([]rune("早")) // 1
}
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

再看一下 go 中 utf-8 编码的具体实现。首先获取字符的码点值,然后根据范围判断字节数,根据对应格式生成编码值。如果是无效的码点值,或码点值位于空段,则返回U+FFFD(即 �)。解码过程不再赘述。

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
// EncodeRune writes into p (which must be large enough) the UTF-8 encoding of the rune.
// It returns the number of bytes written.
func EncodeRune(p []byte, r rune) int {
// Negative values are erroneous. Making it unsigned addresses the problem.
switch i := uint32(r); {
case i <= rune1Max:
p[0] = byte(r)
return 1
case i <= rune2Max:
_ = p[1] // eliminate bounds checks
p[0] = t2 | byte(r>>6)
p[1] = tx | byte(r)&maskx
return 2
case i > MaxRune, surrogateMin <= i && i <= surrogateMax:
r = RuneError
fallthrough
case i <= rune3Max:
_ = p[2] // eliminate bounds checks
p[0] = t3 | byte(r>>12)
p[1] = tx | byte(r>>6)&maskx
p[2] = tx | byte(r)&maskx
return 3
default:
_ = p[3] // eliminate bounds checks
p[0] = t4 | byte(r>>18)
p[1] = tx | byte(r>>12)&maskx
p[2] = tx | byte(r>>6)&maskx
p[3] = tx | byte(r)&maskx
return 4
}
}

const(
t1 = 0b00000000
tx = 0b10000000
t2 = 0b11000000
t3 = 0b11100000
t4 = 0b11110000
t5 = 0b11111000
maskx = 0b00111111
mask2 = 0b00011111
mask3 = 0b00001111
mask4 = 0b00000111
rune1Max = 1<<7 - 1
rune2Max = 1<<11 - 1
rune3Max = 1<<16 - 1
RuneError = '\uFFFD' // the "error" Rune or "Unicode replacement character"
)

// Code points in the surrogate range are not valid for UTF-8.
const (
surrogateMin = 0xD800
surrogateMax = 0xDFFF
)

二进制编码

引言:HTTP 是怎么传输二进制数据的?

  • Step1:定义字符集;

  • Step2:将二进制数据分组;

  • Step3:将每组映射为字符;

字符编码是「文本」变为「二进制」的过程,那如何将任意「二进制」变为「文本」?答案是进行二进制编码,常见有 Hex 编码与 Base64 编码。

显然不能按字符编码直接解码,因为字符编码的结果二进制是满足编码规律的,而非「任意」的,非法格式进行字符解码会出现乱码(比如对0b11xxxxxx进行 UTF-8 解码)。

Hex 编码

Hex 编码是最直观的二进制编码方式,所见即所得。上文中的十六进制表示就是用的 Hex 编码。规则如下:

  1. Hex 字符集为0123456789abcdef;

  2. 每 4bit 为 1 组(2^4=16);

  3. 每组映射为一个 Hex 字符;

计算机中二进制数据都是以字节为单位存储的,1 个字节 8bit,不会出现无法被 4 整除的情况。

每个字节编码为 2 个 Hex 字符,即编码后的字符数是原始数据字节数的 2 倍。在 ASCII 或 UTF-8 编码下,存储 Hex 结果字符串需要的空间是原始数据的 2 倍,存储效率为 50%。

Base64 编码

Base64 编码,顾名思义,是基于 64 个字符进行编码。规则如下:

  1. Base64 字符集(以标准 Base64 为例, 26 大写, 26 小写, 10 数字, 以及+、/)为ABC…YZabc…yz012…89+/;
  2. 每 6bit 为一组(2^6=64),即每 3 个字节为 4 组
  3. 每组映射为一个 Base64 字符;

如果要编码的二进制数据不是 3 的倍数,最后会剩下 1 个或 2 个字节怎么办?**标准编码(StdEncoding)**会先在末尾用 0x00 补齐再分组,并将最后 2 个或 1 个 6bit 分组(全为 0 填充)映射为’=’,表示补齐的 0 字节数量。

img

举个例子,以0x12 34 ab cd编码为标准 base64 为例:

  1. 不足 3 的倍数,先用两个 0 字节补齐 –>0x12 34 ab cd 00 00
  2. 0x12 34 ab编码为EjSr
  3. 0xcd 00 00二进制为0b1100 1101 0000 0000 0000 0000,分为 4 组后为110011 010000 000000 000000,编码结果为zQ==
  4. 最终编码结果为EjSrzQ==

解码过程注意末尾字节的处理即可,此处不再赘述。

  1. EjSrzQ==–>0x12 34 ab cd 00 00–>0x12 34 ab cd

标准编码中编码结果字符长度一定是 4 的倍数,且是原始数据字节数的 4/3 倍,因为会将字节数据补齐至 3 的倍数,每 3 个字节编码为 4 个字符。**在 ASCII 或 UTF-8 编码下,存储结果字符串需要的空间是原始数据的 4/3 倍,存储效率为 75%**。

根据字符集的不同,Base64 编码有几个变种,除了标准编码(StdEncoding),常见的还有 URL 编码(URLEncoding)、原始标准编码(RawStdEncoding)以及原始 URL 编码(RawUrlEncoded)。

简单来说,Raw 指的是无 Padding,URL 指的是用-和_取代编码结果中包含的 url 关键字+和/。不妨参考 Golang 中encoding/base64包中的描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// StdEncoding is the standard base64 encoding, as defined in
// RFC 4648.
var StdEncoding = NewEncoding(*encodeStd*)

// URLEncoding is the alternate base64 encoding defined in RFC 4648.
// It is typically used in URLs and file names.
var URLEncoding = NewEncoding(*encodeURL*)

// RawStdEncoding is the standard raw, unpadded base64 encoding,
// as defined in RFC 4648 section 3.2.
// This is the same as StdEncoding but omits padding characters.
var RawStdEncoding = StdEncoding.WithPadding(*NoPadding*)

// RawURLEncoding is the unpadded alternate base64 encoding defined in RFC 4648.
// It is typically used in URLs and file names.
// This is the same as URLEncoding but omits padding characters.
var RawURLEncoding = URLEncoding.WithPadding(*NoPadding*)

与标准编码不同的是,原始编码中,字节数不足 3 的倍数时不会补齐字节数,采用如下方案:

  1. 如果剩余 1 字节,则左移 4bit 后转换为 2 字符;
  2. 如果剩余 2 字节,则左移 2bit 后转化为 3 字符;

原始编码方案中,结果字符串长度可以不是 4 的倍数

Hex 编码可以看成“Base16 编码”。随着字符数量的增加,存储效率也随之增加。如果有“Base256”编码,存储效率岂不就 100%了?很遗憾,主流字符编码中,单字节能表示的可打印字符只有 92 个。通过扩充多字节字符,或用组合字符实现 base256 意义不大。

Golang 中的二进制编码

看一下 Golang 中 Base64 编码的实现。首先通过EncodedLen方法确定结果长度,生成输出buf,然后通过Encode方法将编码结果填充到buf并返回结果字符串。

1
2
3
4
5
6
// EncodeToString returns the base64 encoding of src.
func (enc *Encoding) EncodeToString(src []byte) string {
buf := make([]byte, enc.EncodedLen(len(src)))
enc.Encode(buf, src)
return string(buf)
}

如前述,标准编码和原始编码(无 Padding)的结果长度不同:如果需要 Padding,直接根据字节数计算即可,反之则需要根据 bit 数计算。

1
2
3
4
5
6
7
8
// EncodedLen returns the length in bytes of the base64 encoding
// of an input buffer of length n.
func (enc *Encoding) EncodedLen(n int) int {
if enc.padChar == *NoPadding* {
return (n*8 + 5) / 6 // minimum # chars at 6 bits per char
}
return (n + 2) / 3 * 4 // minimum # 4-char quanta, 3 bytes each
}

Encode方法实现了编码细节。首先遍历字节数组,将每 3 个字节编码为 4 个字符。最后处理剩余的 1 或 2 个字节(如有):首先使用移位运算进行 0bit 填充,然后进行字符转换。如前述,无 Padding 时,剩下 1 字节对应 2 字符,剩下 2 字节对应 3 字符,即至少会有 2 字符。最后在switch代码段中,根据剩余字节数填充第 3 个字符和 Padding 字符(如有)即可。

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
func (enc *Encoding) Encode(dst, src []byte) {
if len(src) == 0 {
return
}
// enc is a pointer receiver, so the use of enc.encode within the hot
// loop below means a nil check at every operation. Lift that nil check
// outside of the loop to speed up the encoder.
_ = enc.encode
di, si := 0, 0
n := (len(src) / 3) * 3
for si < n {
// Convert 3x 8bit source bytes into 4 bytes
val := uint(src[si+0])<<16 | uint(src[si+1])<<8 | uint(src[si+2])
dst[di+0] = enc.encode[val>>18&0x3F]
dst[di+1] = enc.encode[val>>12&0x3F]
dst[di+2] = enc.encode[val>>6&0x3F]
dst[di+3] = enc.encode[val&0x3F]
si += 3
di += 4
}
remain := len(src) - si
if remain == 0 {
return
}
// Add the remaining small block
val := uint(src[si+0]) << 16
if remain == 2 {
val |= uint(src[si+1]) << 8
}
dst[di+0] = enc.encode[val>>18&0x3F]
dst[di+1] = enc.encode[val>>12&0x3F]
switch remain {
case 2:
dst[di+2] = enc.encode[val>>6&0x3F]
if enc.padChar != *NoPadding* {
dst[di+3] = byte(enc.padChar)
}
case 1:
if enc.padChar != *NoPadding* {
dst[di+2] = byte(enc.padChar)
dst[di+3] = byte(enc.padChar)
}
}
}

字节序

引言:拿到两个字节,如何解析为整形?

  • Step1:明确字节高低位顺序
  • Step2:按高低位权重计算结果

上述二进制编码主要用于文本传输,能不能不进行编码,直接传输二进制?当然可以,基于二进制传输协议,如 TCP 协议。那么什么是文本传输,什么是二进制传输?简单来说,文本传输,内容为文本,自带描述信息(参数名),如 HTTP 中的字段都以 KV 形式存在。二进制传输,内容为二进制,以预先定义好的格式拼在一起,如 TCP 协议报文格式。

img

大端与小端

聊到二进制传输,一个避不开的话题是字节序。什么是字节序?假设读取到一个两字节的 uint16 0x04 0x00,如果从左往右(从高位往低位)解码,得到的是 1024,反过来(从低位往高位)解码则是 4,这就是字节序。符合人类阅读习惯的(从高位往低位)是大端(BigEndian),反之为小端(LittleEndian)。

另一种大小端的定义:LittleEndian 将低序字节存储在低地址,BigEndian 将高序字节存储在低地址。理解起来有些抽象,本质上是一致的。

img

为什么会有小端字节序,统一都用大端不好么?

计算机不这么想,因为计算机中计算都是从低位开始的,电路先处理低位字节效率比较高。但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

那什么时候程序员需要进行字节序处理呢?当多字节整形(uint16,uint32,uint64)需要和字节数组互相转换时。字节数组是无字节序的,客户端写入啥,服务端就读取啥,不会出现逆序,写入和读取无需考虑字节序,这点大可放心只有当多字节整形和字节数组互转时必须指明字节序。

Golang 中的字节序

以 uint16 与字节数组互转为例,看一下 Golang 中 encoding/binary 包中的字节序处理与实现。可见实现并不复杂,注意字节顺序即可。

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
func TestEndian(t *testing.T) {
bytes := make([]byte, 2)
binary.LittleEndian.PutUint16(bytes, 1024) // 小端写 --> 0x0004
binary.BigEndian.PutUint16(bytes, 1024) // 大端写 --> 0x0400
binary.LittleEndian.Uint16(bytes) // 小端读 --> 4
binary.BigEndian.Uint16(bytes) // 大端读 --> 1024
}

func (littleEndian) PutUint16(b []byte, v uint16) {
_ = b[1] // early bounds check to guarantee safety of writes below
b[0] = byte(v)
b[1] = byte(v >> 8)
}

func (bigEndian) PutUint16(b []byte, v uint16) {
_ = b[1] // early bounds check to guarantee safety of writes below
b[0] = byte(v >> 8)
b[1] = byte(v)
}

func (littleEndian) Uint16(b []byte) uint16 {
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
return uint16(b[0]) | uint16(b[1])<<8
}

func (bigEndian) Uint16(b []byte) uint16 {
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
return uint16(b[1]) | uint16(b[0])<<8
}

实战:加解密中的编码与字节序

在加解密场景中,通常会对明文加密得到密文,对密文解密得到明文。比如对密码”123456”(明文)进行对称加密(如 SM4)得到”G7EeTPnuvSU41T68qsuc_g”(密文)。明文和密文都是由可打印字符构成的文本,通常明文人类可直接阅读其含义(不考虑二次加密),密文需要解密后才能理解含义。

那么上述明文变成密文,期间经历了哪些编码过程呢?以加密为例:

  1. 将明文”123456”进行字符解码(如 UTF-8),得到明文字节序列0x31 32 33 34 35 36;
  2. 将明文字节序列输入 SM4 加密算法,输出密文字节序列0x1b b1 1e 4c f9 ee bd 25 38 d5 3e bc aa cb 9c fe;
  3. 将密文字节序列进行二进制编码(如 RawURLBase64),得到密文”G7EeTPnuvSU41T68qsuc_g”;

同理,将”G7EeTPnuvSU41T68qsuc_g”解密成”123456”过程中,应与加密过程的编码方式对应:先进行 RawRULBase64 解码,再解密,最后再进行 UTF-8 编码。

加解密算法的输入输出都是字节序列,所以要将明文、密文与字节序列进行转换。有两点需要注意:

  1. 明文解码为明文字节序列,解码方式因场景而定。对于多次加密场景(如对“G7EeTPnuvSU41T68qsuc_g”再次加密),明文是 Base64 编码得到的,建议采用一致的方式解码。虽然也可以直接进行 UTF-8 解码,但会使加解密流程设计变得复杂。
  2. 密文字节序列编码为密文,必须用二进制编码,不能用字符编码。使用字符编码会产生乱码(意味着数据丢失,无法逆向解码出原始数据)。上述密文序列密文序列进行 UTF-8 编码的结果是 �L���%8�>��˜�。

合规要求,加解密场景中应使用硬件加密机。通常硬件加密机提供基于 TCP 的字节流通信方式,比如约定每次通信数据中的前 2 字节为数据长度,后面的为真实数据。发送时,需要将真实数据长度转为 2 字节拼在前面,接收时,需要先读取前两字节得到真实数据长度 N,再读取 N 字节得到真实数据。其中长度与字节序列的转换需要关注字节序:发送方和接收方的字节序处理保持一致即可,比如全用大端。下面给出了数据发送的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
func (m *EncryptMachine) sendData(conn net.Conn, data []byte) error {
// add length
newData := m.addLength(data)
// send new data
return util.SocketWriteData(conn, newData)
}

func (m *EncryptMachine) addLength(data []byte) []byte {
lengthBytes := make([]byte, 2)
binary.BigEndian.PutUint16(lengthBytes, uint16(len(data)))
return append(lengthBytes, data...)
}

总结

编码虽然基础,但却容易出错,切莫眼高手低。希望本文能帮助大家进一步了解字符编码、二进制编码与字节序,避免踩坑。

未完,待续

Go语言参考手册

介绍

这是一个 Go 语言的参考手册,你也可以访问golang.org获取更多信息和其他文档。

Go 是在设计时考虑了系统编程的通用型编程语言。它是强类型,有垃圾回收机制并原生支持并发编程。Go 程序由一个或多个 package 组成,这样可以高效的管理依赖。

Go 的语法简洁且有规则,这让自动化工具可以很容易的分析代码,例如:集成开发环境。

标记

语法采用扩展巴科斯范式。

1
2
3
4
5
6
7
Production  = production_name "=" [ Expression ] "." .
Expression = Alternative { "|" Alternative } .
Alternative = Term { Term } .
Term = production_name | token [ "…" token ] | Group | Option | Repetition .
Group = "(" Expression ")" .
Option = "[" Expression "]" .
Repetition = "{" Expression "}" .

产生式是由词法单元和以下操作符构成的表达式(优先级依次递增):

1
2
3
4
|   或
() 分组
[] 可选 (出现 01 次)
{} 可重复 (出现 0 到 n 次)

小写的产生式名称用来与词法单元区分。非终结符采用驼峰式。词法单元由双引号或反引号组成。

a...b 表示从 ab 之间的任意字符。省略号 ... 也可以在规范中表示对更详细的枚举和代码片段的省略。字符 ... 不是 Go 语言的词法单元。

源码表示法

Go 的源代码使用 UTF-8 编码的 Unicode 文本。不过它并不是完全规范化的,单重音的代码点与由相同字符和音标组成的代码点是不同的;前者我们认为它是两个代码点。简单来讲,文档会在源代码文本中使用非规范的术语字符来表示一个 Unicode 代码点。

每个代码点都是不同的;相同字符的大写和小写形式表示不同的字符。

实现限制:为了兼容其他工具,编译器不允许出现 Utf-8 编码的源文本中的 NUL 字符(U+0000)。

实现限制:为了兼容其他工具,如果源文本中是以Utf-8 编码的字节序标记(U+FEFF)为起始代码点。编译器会忽略它。字节序标记不应出现在源文本的任何位置。

字符

这些单词表示 Unicode 字符的类别:

1
2
3
4
newline        = /* Unicode 代码点 U+000A */ .
unicode_char = /* 排除换行以外的任意 Unicode 代码点 */ .
unicode_letter = /* 一个字母("Letter")类型的 Unicode 代码点 */ .
unicode_digit = /* 一个数字("Number, decimal digit")类型的 Unicode 代码点 */ .

在 Unicode8.0 标准中,第 4.5 章节 “一般类别” 中定义了字符的类别。Go 能够处理任何字符集,包括 Lu,Li,Lt,Lm 或 Lo 作为 Unicode 字母,还可以把数字字符集 Nd 当作 Unicode 数字处理。

字母和数字

我们认为下划线 _ (U+005F)是一个字母:

1
2
3
4
letter        = unicode_letter | "_" .
decimal_digit = "0""9" .
octal_digit = "0""7" .
hex_digit = "0""9" | "A""F" | "a""f" .

词汇元素

注释

注释是程序的说明文档。在 Go 中有两种形式:

  • 单行注释从 // 开始直到行末结束。
  • 通用注释从 /* 开始直到 */ 结束。

注释不能嵌套在其他注释、字符串和 rune 的字面值中。不包含换行符的通用注释之间通过空格符连接,其他情况下每段注释都会另起一行。

词汇元素

词汇元素构成了 Go 语言的词汇表。它有四种类型:标识符、关键字、操作符/标点符号、字面值。空白符可以是空格(U+0020)、水平制表符(U+0009)、换行符(U+000D)或换行符(U+000A)。它本身会被忽略,一般用来区分不同的词汇元素。换行符或文件终止符(EOF)还可能触发编译程序在源代码的行末或文件末尾追加分号。在分解源代码的词汇元素的过程中,会把当前可以形成有效词汇元素的最长字符序列作为下一个词汇元素。

分号

正规语法在很多产生式中使用分号 “;” 作为终结符。Go 程序中遵循下面两条规则省略了大部分的分号:

  1. 当某行的最后一个词汇元素是以下元素时自动补全分号:
  • 一个标识符。

  • 一个整数,浮点数,虚数,rune 或字符串字面值。

  • 关键字 breakcontinuefallthroughreturn 其中之一。

  • 操作符/标点符号 ++--)]} 其中之一。

  1. 为了支持独占一行的复杂语句,会省略与 “)” 或 “}” 相邻的分号。

为了反应惯用用途,本篇文档的所有例子都基于以上规则省略分号。

标识符

标识符表示程序实体单元,例如:变量、类型。一个标识符由一个或多个字母和数字组成。标识符的首字符必须为字母。

1
identifier = letter { letter | unicode_digit } .
1
2
3
4
a
_x9
ThisVariableIsExported
αβ

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

操作符和标点符号

以下字符序列用于表示操作符(包括赋值运算符)和标点符号:

1
2
3
4
5
6
+    &     +=    &=     &&    ==    !=    (    )
- | -= |= || < <= [ ]
* ^ *= ^= <- > >= { }
/ << /= <<= ++ = := , ;
% >> %= >>= -- ! ... . :
&^ &^=

整型字面值

整型字面值是一个数字序列,相当于整型常量。可以使用前缀指定非小数进制:0 表示八进制,0x/0X 表示十六进制。在十六进制字面值中,字母 a-f 和 A-F 都表示数字 10-15。

1
2
3
4
int_lit     = decimal_lit | octal_lit | hex_lit .
decimal_lit = ( "1""9" ) { decimal_digit } .
octal_lit = "0" { octal_digit } .
hex_lit = "0" ( "x" | "X" ) hex_digit { hex_digit } .
1
2
3
4
42
0600
0xBadFace
170141183460469231731687303715884105727

浮点字面值

浮点字面值是一个小数,相当于浮点数常量。它由整数部分,小数点,小数部分和指数部分构成。整数部分和小数部分用小数点链接;指数部分由 e / E 字符后接一个有符号指数构成。整数部分和小数部分可以省略其一;小数点和指数部分可以省略其一。

1
2
3
4
5
float_lit = decimals "." [ decimals ] [ exponent ] |
decimals exponent |
"." decimals [ exponent ] .
decimals = decimal_digit { decimal_digit } .
exponent = ( "e" | "E" ) [ "+" | "-" ] decimals .
1
2
3
4
5
6
7
8
9
0.
72.40
072.40 // == 72.40
2.71828
1.e+0
6.67428e-11
1E6
.25
.12345E+5

虚数字面值

虚数字面值是一个小数,相当于复数常量中的虚数部分。它由浮点数或者整数后接小写字母 i 构成。

1
imaginary_lit = (decimals | float_lit) "i" .
1
2
3
4
5
6
7
8
9
0i
011i // == 11i
0.i
2.71828i
1.e+0i
6.67428e-11i
1E6i
.25i
.12345E+5i

Rune 字面值

rune 类型字面值相当于一个 rune 常量。它是一个表示 Unicode 代码点的整数。rune 类型字面值表示为用单引号包裹的一个或多个字符,像 ‘x’ 或 ‘\n’。在单引号中除了换行符和未转义的单引号其他的字符都可以直接显示。单引号包裹的字符的值和字符在 Unicode 编码中的值相等,而以反斜线开头的多字符序列会把值翻译成多种格式。

使用引号表示单字符是最简单的方式;因为 Go 的源文本是 UTF-8 编码,一个整数可能代表多个 UTF-8 字节。例如, ‘a’ 可以使用单字节表示字符 a,Unicode 编码 U+0061,值 0x61,而 ‘ä’ 是两字节表示分音符的 a,Unicode 编码 U+00E4,值 0xe4。

反斜线能将任意值编码成 ASCII 文本。有四种方式将整数值表示为数字常量:\x 后接两个十六进制数;\u 后接四个十六进制数;\U 后接八个十六进制数。 \ 后接三个八进制数。每种情况下都使用相应进制来表示字面量的整数值。

虽然这四种方式都以整数表示,但它们的有效区间并不相同。八进制只能表示 0 - 255 以内的整数。十六进制满可以满足需求。\u\U 都可以表示 Unicode 代码点,不过其中的一些值是无效的,特别是 0x10FFFF 以上的值。

反斜线结合以下字符具有特殊含义:

1
2
3
4
5
6
7
8
9
10
\a   U+0007 alert or bell
\b U+0008 退格符
\f U+000C form feed
\n U+000A line feed or newline
\r U+000D carriage return
\t U+0009 水平制表符
\v U+000b 垂直制表符
\\ U+005c 反斜线
\' U+0027 单引号 (只在 rune 字面值中有效)
\" U+0022 双引号 (只在字符串字面值中有效)

其他所有以反斜线开头的序列在 rune 的规则中都是非法的。

1
2
3
4
5
6
7
8
9
rune_lit         = "'" ( unicode_value | byte_value ) "'" .
unicode_value = unicode_char | little_u_value | big_u_value | escaped_char .
byte_value = octal_byte_value | hex_byte_value .
octal_byte_value = `\` octal_digit octal_digit octal_digit .
hex_byte_value = `\` "x" hex_digit hex_digit .
little_u_value = `\` "u" hex_digit hex_digit hex_digit hex_digit .
big_u_value = `\` "U" hex_digit hex_digit hex_digit hex_digit
hex_digit hex_digit hex_digit hex_digit .
escaped_char = `\` ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | `\` | "'" | `"` ) .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'a'
'ä'
'本'
'\t'
'\000'
'\007'
'\377'
'\x07'
'\xff'
'\u12e4'
'\U00101234'
'\'' // 包含单引号的 rune 字面值
'aa' // 无效: 太多字符
'\xa' // 无效: 缺少十六进制数
'\0' // 无效: 缺少八进制数
'\uDFFF' // 无效: surrogate half
'\U00110000' // 无效: 非法的 Unicode 代码点

字符串字面量

字符串字面量表示从字符序列中获取的字符串常量。它有两种格式:原始字符串字面量和解释型字符串字面量。

原始字符串是由反引号包裹(foo)。字符串中除反引号以外的其他字符都会显示出来。原生字符串由反引号之间的(默认 UTF-8 编码)的字符组成。它的值为引号内未经解释(默认 UTF-8 编码)所有字符;尤其是,反斜线再字符串中没有特殊意义并且字符串中保留换行符。在原始字符串的值中会丢弃回车键返回 ‘\r’ 字符。

解释型字符串由双引号之间的字符组成(”bar”)。除了换行符和双引号其他字符都会显示出来。双引号之间的文本组成字面量的值。反斜线的转义规则与 rune 字面量基本相同(不同的是 \’ 非法,而 " 合法)。三位八进制数(\nnn)和两位十六进制数(\xnn)换码符的值表示相应字符串的字节。其他的换码符都表示字符各自的 UTF-8 编码(可能是多字节)。因此字符串 \377 和 \xFF 都表示值为 0xFF=255 的单个字节,而 ÿ, \u00FF, \U000000FF\xc3\xbf 表示 UTF-8 编码字符 U+00FF 的两个字节 0xc3 0xbf。

1
2
3
string_lit             = raw_string_lit | interpreted_string_lit .
raw_string_lit = "`" { unicode_char | newline } "`" .
interpreted_string_lit = `"` { unicode_value | byte_value } `"` .
1
2
3
4
5
6
7
8
9
10
11
`abc`                // 等价于 "abc"
`\n
\n` // 等价于 "\\n\n\\n"
"\n"
"\"" // 等价于 `"`
"Hello, world!\n"
"日本語"
"\u65e5本\U00008a9e"
"\xff\u00FF"
"\uD800" // 无效: surrogate half
"\U00110000" // 无效: 无效的 Unicode 代码点

这些例子都表示相同的字符串:

1
2
3
4
5
"日本語"                                 // UTF-8 文本
`日本語` // UTF-8 文本作为原生字面值
"\u65e5\u672c\u8a9e" // 确定的 Unicode 代码点
"\U000065e5\U0000672c\U00008a9e" // 确定的 Unicode 代码点
"\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e" // 确定的 UTF-8 字节

如果源代码中使用两个代码点表示一个字符,例如带音标的字母,把它放在 rune 中会报错(它不是单代码点)。并且在字符串中会显示两个代码点。

常量

常量分为:布尔型,rune型,整型,浮点型,复数型,字符串型。其中 rune,整型,浮点型,复数型统称为数字常量。

常量的值可以表示为一个 rune字面量,整数字面量,浮点数字面量,虚数字面量,字符串字面量,表示常量的标识符,常量表达式,一个转换结果为常量的类型转换,和一些返回值为常量的内置函数(接受任何值的unsafe.Sizeof,接受部分表达式的caplen,接受虚数常量的realimag,接受数字常量的 complex)。布尔类型的值为预定义常量 truefalse,预定义的标识符 iota 表示一个整型常量。

一般情况下复数常量是常量表达式的一种形式。会在常量表达式章节详细讨论。

数字常量可以表示任意精度的确定值而且不会溢出。因此,没有常量可以表示非 0,无穷大和非数字值。

常量可以指定类型也可以不指定类型。字面值常量,truefalseiota,和只包含无类型常量操作的常量表达式是无类型的。

常量可以通过常量声明和转换时显式的指定具体类型,也可以隐式的在变量声明、赋值或作为表达式操作元时隐式的指定具体类型。如果常量的值和他的类型不匹配,会报错。

无类型常量由一个默认的类型,这个类型会根据使用常量时的上下文进行隐式转换。例如:短变量声明 i := 0 没有指定 i 的类型。无类型常量的默认类型可以是:boolruneintfloat64complex128 或者 string,具体选择哪种类型由常量的值决定。

实现限制:虽然数字常量在 Go 中是任意精度,不过编译器在实现时会在内部限制精度。这意味着每个编译器实现都要:

  • 至少保证整形常量有 256 位

  • 浮点数常量(包括复数常量)都要保证至少 256 位的主体部分和至少 16 位的有符号指数部分

  • 如果不能表示给定整数的精度抛出错误

  • 如果浮点数或复数溢出抛出错误

  • 如果由于精度限制不能表示浮点数或者复数进行舍入

这些要求同时作用于字面量常量额和常量表达式的结果。

变量

变量是一个用来储存值的位置。根据不同的变量类型,可以保存不同的值。

变量声明,函数参数和返回值,声明的函数签名,函数字面值都会为命名变量预留储存空间。调用内置的 new 函数或获取复合字面值的地址都会在运行时为变量分配存储空间。这种匿名变量是通过(可能是隐式的)指针间接引用的。

像数组,切片和结构体类型的变量,它们内部都包含很多元素或字段,而且这些元素和字段都可以直接被访问。数组和切片中的每个元素的行为和单独的变量基本相同。

变量的静态类型可以通过变量声明、提供给 new 的类型、复合字面值、结构体变量声明的元素类型以上几种方式确定。通过new或者类型初始化。接口类型的变量也有一个明确的动态类型,这个动态类型是在运行时赋值给变量的具体值类型(特例:预声明的 nil 是无类型的)。动态类型在程序的执行过程中可能并不相同,但是接口变量的值是可以分配给相同静态类型的变量。

1
2
3
4
var x interface{}  // x 的静态类型为 interface{} 值为 nil
var v *T // v 的静态类型为 *T 值为 nil
x = 42 // x 的动态类型为 int 值为 42
x = v // x 动态类型为 *T 值为 (*T)(nil)

在表达式中使用变量可以取出变量的值;这个值就是变量最近一次被赋予的值。如果没有对变量赋过值,那么他的值是该类型的零值。

类型

类型是一个集合,集合包括值和针对值的操作&方法。一个类型可以使用类型名来表示。类型有多种表现形式:如果存在类型名,可以使用类型名表示,或者也可以使用根据已有类型组合成的类型字面值。

1
2
3
4
Type      = TypeName | TypeLit | "(" Type ")" .
TypeName = identifier | QualifiedIdent .
TypeLit = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
SliceType | MapType | ChannelType .

Go 已经预先声明了某些类型的名称。并引入了类型声明。复合类型(数组、结构体、指针、函数、接口、切片、map、channel)可以使用他们的类型字面值。

每个类型T都有一个底层类型。如果T是预定义类型或者类型字面值。那么底层类型就是他自身。否则,T的底层类型就是它再类型声明时引用到的类型。

1
2
3
4
5
6
7
8
9
10
11
type (
A1 = string
A2 = A1
)

type (
B1 string
B2 B1
B3 []B1
B4 B3
)

stringA1A2B1B2 的底层类型是 string[]B1B3B4 的下游类型是[]B1。

方法集

类型可能会有一个与之关联的方法集。接口类型的方法集就可以使用自身表示。对于其他类型,类型 T 的方法集由所有接收者类型为 T 的方法组成。而对应指针类型 *T 的方法集由所有接收者类型为 T 或 *T 的方法组成。如果是结构体类型且含有嵌入字段,那么方法集中可能还会包含更多的方法,具体请看结构体类型章节。其他类型的方法集都为空。方法集中的每个方法都有唯一且不为空的方法名。

类型的方法集用来确定类型实现的接口和以类型作为接收者能够调用的方法。

布尔类型

布尔类型表示预定义常量 truefalse 表示布尔真实值的集合。预定义的布尔类型为 bool;它是通过类型声明创建的。

数字类型

一个数字类型相当于整型和浮点型的所有值的集合。预定义的数字类型包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
uint8       8 位无符号整数集合 (0 to 255)
uint16 16 位无符号整数集合 (0 to 65535)
uint32 32 位无符号整数集合 (0 to 4294967295)
uint64 64 位无符号整数集合 (0 to 18446744073709551615)

int8 8 位有符号整数集合 (-128 to 127)
int16 16 位有符号整数集合 (-32768 to 32767)
int32 32 位有符号整数集合 (-2147483648 to 2147483647)
int64 64 位有符号整数集合 (-9223372036854775808 to 9223372036854775807)

float32 IEEE-754 32 位浮点数集合
float64 IEEE-754 64 位浮点数集合

complex64 实部虚部都为 float32 的复数集合
complex128 实部虚部都为 float64 的复数集合

byte uint8 的别名
rune int32 的别名

n 位整数的值具有 n 比特的宽度并用补码表示。

以下几种预定义类型由具体平台实现指定长度:

1
2
3
uint     3264
intuint 位数相同
uintptr 能够容纳指针值的无符号整数

为了避免移植性问题,除了被 uint8 的别名 byte 和 int32 的别名 rune,其他所有的数字类型都是通过类型声明定义。当在表达式中使用不同的数字类型需要进行类型转换。例如:int32 和 int 不是相同的类型,即使他们在指定的平台上是相等的。

字符串类型

字符串类型表示字符串的值类型。字符串的值是一个字节序列(有可能为空)。字符串一旦创建就无法修改它的值。预定义的字符串类型是 string,它是通过类型声明定义的。

可以使用内置函数 len 获取字符串长度。如果字符串是常量那么它的长度在编译时也为常量。可以通过数字下标 0~len(s)-1 访问字符串字节。获取字符串的地址是非法操作;如果 s[i] 是字符串的第 i 个字节,那么 &s[i] 是无效的。

数组类型

数组是一定数量的单一类型元素序列,而这个单一类型叫做元素类型。元素的个数表示元素的长度,它永远不是负数。

1
2
3
ArrayType   = "[" ArrayLength "]" ElementType .
ArrayLength = Expression .
ElementType = Type .

长度是数组类型的一部分;它是一个类型为 int 的非负常量。可以用内置函数 len 获取数组的长度。元素可以通过下标 0~len(a)-1 访问。数组一般都是一维的,不过也可以是多维的。

1
2
3
4
5
[32]byte
[2*N] struct { x, y int32 }
[1000]*float64
[3][5]int
[2][2][2]float64 // same as [2]([2]([2]float64))

切片类型

切片描述了底层数组的一个连续片段并提供对连续片段内元素的访问。切片类型表示元素类型的数组的所有切片的集合。没有被初始化的切片用 nil 表示。

1
SliceType = "[" "]" ElementType .

与数组一样,切片的可以使用索引访问并且有长度,切片的长度可以通过内置的 len 函数获取;与数组不同的是它的长度在运行时是可以变化的。我们可以通过下标 0~len(s)-1 来访问切片内的元素。切片的索引可能会小于相同元素再底层数组的索引。

切片一旦初始化,那么就有一个与之对应的底层数组保存切片中的元素。切片和底层的数组还有其他指向该数组的切片共享相同的储存空间;而不同的数组总是有着不同的存储空间。

切片的底层数组可能会延伸到切片末尾以外,切片的容积等于切片现在的长度加上数组中切片还没使用的长度;可以从原始切片中切出一个长度与容量相等的切片。切片的容量可以通过内置的 cap(a) 函数来获取。可以通过函数make来创建一个T类型的新切片。

使用内置函数 make 可以出实话给定元素类型 T 的切片。make 函数接收三个参数:切片类型、切片长度、切片容积,其中切片容积是可选参数。make 创建的切片会在底层分配一个切片所引用的新数组。

1
make([]T, length, capacity)

make 的作用就是创建新数组并切分它,所以下面两种写法是等价的:

1
2
make([]int, 50, 100)
new([100]int)[0:50]

与数组相同,切片一般是一维的,不过也可以复合成多维。数组中的数组都必须是相同的长度,但是切片中的切片长度是动态变化的,不过切片中的切片需要单独初始化。

结构体类型

结构体是一个命名元素序列,命名元素也叫做字段,每个字段都对应一个名称和类型,字段的名字可以是显式指定的(标识符列表)也可以是隐式的(嵌入字段)。在结构体中非空字段具有唯一性。

1
2
3
4
StructType    = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl = (IdentifierList Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName .
Tag = string_lit .
1
2
3
4
5
6
7
8
9
10
11
// 空结构体.
struct {}

// 6个字段的结构体.
struct {
x, y int
u float32
_ float32 // padding
A *[]int
F func()
}

一个指定了类型而没有指定名称的字段叫做嵌入字段,嵌入字段必须指定类型名 T 或指向非接口类型的指针类型 *T,其中 T 不能为指针类型。或者一个非接口类型的指针。并且T本身不能为指针类型。这种情况下会把类型名作为字段的名字。

1
2
3
4
5
6
7
8
// 一个包含 4 个嵌入字段 T1, *T2, P.T3 和 *P.T4 的结构体
struct {
T1 // 字段名为 T1
*T2 // 字段名为 T2
P.T3 // 字段名为 T3
*P.T4 // 字段名为 T4
x, y int // 字段名为 x 和 y
}

以下声明是错误的因为字段名称必须唯一。

1
2
3
4
5
struct {
T // 嵌入字段 *T 与 *P.T 冲突
*T // 嵌入字段 T 与 *P.T 冲突
*P.T // 嵌入字段 T 与 *T 冲突
}

如果 x.f 是表示该字段或方法 f 的合法选择器,则会调用结构 x 中嵌入字段的字段或方法 f

从嵌入字段组合来的字段与结构体原来的字段行为基本相同,只是不能在结构体的复合字面值中直接使用。

给定一个结构体 S 和一个类型 T,依据以下规则生成组合后的方法集:

  • 如果 S 包含嵌入字段 T,则 S 和 *S 的方法集包括接收者为 T 的方法集,而 *S 包括 接收者为 *T 的方法集。
  • 如果 S 包含字段 T。那么S和S均包含接收者为 T 和 *T 的所有方法集。

声明字段时可以给该字段添加一个字符串的 tag。这个 tag 将会成为它所对应字段的一个属性。空 tag 和缺省 tag 是相同的。tag 的值可以通过反射的接口获取,可以作为类型结构体的类型定义的一部分,也可以忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct {
x, y float64 "" // 空 tag 和缺省 tag 相同
name string "any string is permitted as a tag"
_ [4]byte "ceci n'est pas un champ de structure"
}

// 结构体对应一个 TimeStamp 的 protocol buffer.
// tag 字符串中定义了 protocol buffer 字段对应的数字;
// 一般使用 reflect 包读取他们.
struct {
microsec uint64 `protobuf:"1"`
serverIP6 uint64 `protobuf:"2"`
}

指针类型

指针类型表示所有指向给定类型变量的指针集合。这个指定的类型叫做指针的基础类型。没有初始化的指针值为nil。

1
2
PointerType = "*" BaseType .
BaseType = Type .
1
2
*Point
*[4]int

函数类型

函数类型可以表示所有具有相同参数类型和返回值类型的函数。未初始化的函数类型值为 nil。

1
2
3
4
5
6
FunctionType   = "func" Signature .
Signature = Parameters [ Result ] .
Result = Parameters | Type .
Parameters = "(" [ ParameterList [ "," ] ] ")" .
ParameterList = ParameterDecl { "," ParameterDecl } .
ParameterDecl = [ IdentifierList ] [ "..." ] Type .

在参数和返回值列表中,标识符列表必须同时存在或缺省。如果存在,那么每个名字都表示指定类型的一个参数/返回值,这些标识符必须非空并且不能重复。如果缺省,指定类型的参数/返回值使用对应的类型表示。参数列表和返回值列表一般都是需要加括号,不过在只有一个缺省返回值时,它可以不使用括号。

函数的最后一个参数可以添加前缀 ...。包含这种参数的函数叫做变参函数,它可以接收零个或多个参数。

1
2
3
4
5
6
7
8
func()
func(x int) int
func(a, _ int, z float32) bool
func(a, b int, z float32) (bool)
func(prefix string, values ...int)
func(a, b int, z float64, opt ...interface{}) (success bool)
func(int, int, float64) (float64, *[]int)
func(n int) func(p *T)

接口类型

接口类型指定了一个方法集。一个接口类型变量可以保存任何方法集是该接口超集的类型。我们可以认为类型实现了接口。没有初始化的接口类型值为 nil。

1
2
3
4
InterfaceType      = "interface" "{" { MethodSpec ";" } "}" .
MethodSpec = MethodName Signature | InterfaceTypeName .
MethodName = identifier .
InterfaceTypeName = TypeName .

在接口类型的方法集中,每个方法的名称必须是非空且唯一。

1
2
3
4
5
6
// A simple File interface
interface {
Read(b Buffer) bool
Write(b Buffer) bool
Close()
}

接口可以由多个类型实现,例如:类型 S1 和类型 S2 都有以下方法集:

1
2
3
func (p T) Read(b Buffer) bool { return … }
func (p T) Write(b Buffer) bool { return … }
func (p T) Close() { … }

(这里的类型 T 可以表示 S1 也可以表示 S2S1S2 都实现了接口 File,而不用管类型是否还有其他方法。

一个类型实现了任何方法集的为其子集的接口。因此它可能实现了多个不同接口。例如:所有的类型都实现了空接口:

1
interface{}

与之相似,思考下面这个定义为 Locker 的接口:

1
2
3
4
type Locker interface {
Lock()
Unlock()
}

如果 S1S2 也实现了它:

1
2
func (p T) Lock() { … }
func (p T) Unlock() { … }

那它们就实现了两个接口 LockerFile

一个接口 T 可以使用另一个接口 E 来指定方法。这种方式叫做将接口 E 嵌入进接口 T。它把 E 中所有的方法(包括导出和未导出的方法)全部添加进接口 T。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type ReadWriter interface {
Read(b Buffer) bool
Write(b Buffer) bool
}

type File interface {
ReadWriter // 与添加 ReadWriter 接口中的方法是等价的
Locker // 与添加 Locker 接口中的方法是等价的
Close()
}

type LockedFile interface {
Locker
File // 无效: Lock, Unlock 不是唯一的
Lock() // 无效: Lock 不是唯一的
}

接口 T 不能递归的嵌入进自己或已经嵌入过它的接口。

1
2
3
4
5
6
7
8
9
10
11
12
// 无效: Bad 不能嵌入它自己
type Bad interface {
Bad
}

// 无效: Bad1 不能嵌入已经引用它的 Bad2
type Bad1 interface {
Bad2
}
type Bad2 interface {
Bad1
}

Map类型

map 类型是一种以唯一值作为键的无序集合。

1
2
MapType     = "map" "[" KeyType "]" ElementType .
KeyType = Type .

map的键类型必须能使用比较运算符 ==!= 进行比较。因此它的键类型不能是函数,map,或者切片。如果键是接口类型,那么比较运算符必须能比较他的动态值。如果不能会抛出一个运行时错误。

1
2
3
map[string]int
map[*T]struct{ x, y float64 }
map[string]interface{}

map中元素的个数叫做它的长度。对于一个map m。它的长度可以通过内置函数 len 获得,而且它的长度可能再运行时发生变化。map 可以再运行时添加和取回元素,页可以使用内置函数 delete移除元素。

可以使用内置函数 make 初始化一个新的且为空的 map。它能指定 map 的类型和预留的空间:

1
2
make(map[string]int)
make(map[string]int, 100)

map 的预留空间不会固定住 map 的长度;它可以通过添加一定数量的元素来增加自己的长度(nil map 不能添加元素)。nil map 和空 map 是相等的,只是 nil map 不能添加元素。

Channel类型

channel提供一种手段在并发执行的函数间发送和接收指定类型的值。没有初始化的 channel 是nil。

1
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

操作符 <- 可以指定 channel 的数据流动方向。如果没有指定方向,channel 默认是双向的。channel 可以通过转换和赋值来限制只读和只写。

1
2
3
chan T          // 可以接收和发送 T 类型的数据
chan<- float64 // 只能发送 float64 类型的值
<-chan int // 只能接收

<- 与最左侧的 chan 关联:

1
2
3
4
chan<- chan int    // 等价于 chan<- (chan int)
chan<- <-chan int // 等价于 chan<- (<-chan int)
<-chan <-chan int // 等价于 <-chan (<-chan int)
chan (<-chan int)

可以通过内置的 make 函数初始化 channel。make 函数可以指定channel的类型和容量。

1
make(chan int, 100)

容量是设置了最大能缓存元素的数量。如果没有设置容量或值为 0,channel 就是没有缓存的,这时只有当发送者和接收者都准备好后才会传输数据。而带缓存的 channel 在缓存没有满的时候依然可以成功发送数据,当缓存不为空的时候可以成功接收到数据,值为 nil 的 channel 不能传输数据。

可以通过内置函数 close 关闭 channel。在接收端的第二个返回值可以用来提示接收者在关闭的 channel 是否还包含数据。

channel 可以在发送语句,接收操作中使用。可以不考虑同步性直接在多个 goroutine 中对 channel 调用内置函数 lencap 。channel 的行为和 FIFO 队列相同。举个例子,一个 goruntine 发送数据,另一个 goruntine 接收他们,接收数据的顺序和发送数据的顺序是相同的。

类型的属性和值

类型标识

两个类型可能相同也可能不同。

定义的类型都是不同类型。如果两个类型的底层类型在结构上是相同的,那它们也是相等的。总的来说:

  • 2 个数组的长度和元素类型相同,那么它们就是相同类型。

  • 如果两个切片的元素类型相同那么它们就是相同类型。

  • 如果两个结构体字段顺序相同,并且字段名称、字段类型和 tag 都相同那么它们就是相等的。非导出字段的字段名在不同的包中总是不同的。

  • 如果两个指针的基础类型相同那么他们具有相同类型。

  • 如果两个函数具有相同的参数和返回值列表,并且他们的类型相同那么他们就是相同的,参数的名称不一定要相同。

  • 如果两个接口的方法集完全相同(方法的顺序)。

  • 如果两个 map 类型的键类型和值类型相同那它们就是相等的。

  • 如果两个 channel 类型包含的对象类型和 channel 的方向都是相同的那它们就是相同的。

给出下列声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type (
A0 = []string
A1 = A0
A2 = struct{ a, b int }
A3 = int
A4 = func(A3, float64) *A0
A5 = func(x int, _ float64) *[]string
)

type (
B0 A0
B1 []string
B2 struct{ a, b int }
B3 struct{ a, c int }
B4 func(int, float64) *B0
B5 func(x int, y float64) *A1
)

type C0 = B0

这些类型是相等的:

1
2
3
4
5
6
7
8
9
A0, A1, and []string
A2 and struct{ a, b int }
A3 and int
A4, func(int, float64) *[]string, and A5

B0, B0, and C0
[]int and []int
struct{ a, b *T5 } and struct{ a, b *T5 }
func(x int, y float64) *[]string, func(int, float64) (result *[]string), and A5

B0 和 B1 不是一种类型因为它们是通过类型定义方式分别定义的;func(int, float64) *B0func(x int, y float64) *[]string 是不同的,因为 B0 和 []string 不是相同类型。

可分配性

在以下情况下,可以将 x 分配给类型为 T 的变量(把 x 分配给 T):

  • x 的类型为 T

  • x 的类型 V 和 T 有相同的底层类型并且类型 T 或 V 至少一个定义的类型

  • T 是一个接口类型并且 x 实现了 T

  • x 是一个 channel,并且 T 是channel类型,类型V和类型T有相同的元素类型,并且 2 种类型至少有一种不是定义的类型

  • x 等于 nil 并且 T 是一个指针,函数,切片,map,channel 或接口类型

  • x 是一个可以表示 T 类型值的无类型常量

代表性

满足以下条件时可以用 T 类型的值表示常量 x:

  • T 值的集合包括 x

  • T 是浮点型,而 x 在没有溢出的情况下能够近似成 T 类型。近似规则使用 IEEE 754 round-to-even,负零和无符号的零相同。需要注意的是,常量的值不会为负零,NaN,或无限值。

  • T 为复数类型,并且 x 的 real(x)imag(x) 部分由复数类型对应的浮点类型(float32float64 )组成。

1
2
3
4
5
6
7
8
9
10
11
12
x                   T           x 可以表示 T 的值,因为:

'a' byte 97byte 类型值的集合中
97 rune runeint32 的别名,9732 位整型值的集合中
"foo" string "foo" 在字符串值的集合中
1024 int16 102416 位整型值的集合中
42.0 byte 428 位无符号整型值的集合中
1e10 uint64 1000000000064 位无符号整型值的集合中
2.718281828459045 float32 2.718281828459045 的近似值 2.7182817float32 类型值的集合中
-1e-1000 float64 -1e-1000 的近视值 IEEE -0.0,等于 0
0i int 0 是整型值
(42 + 0i) float32 42.0 (0 虚部) 在 float32 类型值的集合中
1
2
3
4
5
6
7
8
9
x                   T           x 不能表示 T 的值,因为:

0 bool 0 不在布尔值的集合中
'a' string 'a'rune 类型, 它不在字符串类型的值集合中
1024 byte 1024 不在 8 位无符号整型值的集合中
-1 uint16 -1 不在 16 位无符号整型值的集合中
1.1 int 1.1 不是整型值
42i float32 (0 + 42i) 不在 float32 类型值的集合中
1e1000 float64 1e1000 取近似值时会溢出成 IEEE

代码块

代码块是用大括号括起来的声明和语句。

1
2
Block = "{" StatementList "}" .
StatementList = { Statement ";" } .

除了源码中显式的代码块,也有一些隐式的代码块。

  • 包含所有的Go代码的全局代码块。

  • 包含所有包的代码的包代码块。

  • 包含文件内的所有代码的文件代码块。

  • 每个 if,switch和 for 的范围都会形成隐式的块。

  • 每个 switch 和 select 条件都有自己的代码块。

代码块可以嵌套并且影响作用域。

声明和作用域

一段声明可以给常量,类型,变量,函数,标签,和包绑定标识符。程序中每个标识符都需要声明。相同标识符不能在同一个代码块中声明2次。并且相同标识符不能同时在文件和 package 代码块中声明。

空标识符可以和其他标识符一样在声明中使用。不过它不绑定标识符,等于没有声明。在 package 代码块中 init 标识符只能用做 init 函数的标识符,就像空标识符一样,它不会引入新的绑定。

1
2
Declaration   = ConstDecl | TypeDecl | VarDecl .
TopLevelDecl = Declaration | FunctionDecl | MethodDecl .

声明过的标识符的作用域就是声明标识符所在的作用域。

go使用块来规定词汇的方位:

  • 预定义的标识符具有全局作用域。

  • 所有定义的顶级标识符具有包作用域。

  • import进来的包的名字标识符具有文件作用域。

  • 方法的接收者,函数参数,返回值变量具有函数作用域。

  • 函数内定义的参量和变量标识符的作用域是标识符被声明到容纳他的块结束。

一个代码块中声明的标识符可以在它内部的代码块中重新声明。在内部代码块的作用域中标识符表示在内部代码块中声明的实体。

pakcage 语句不属于声明。包名不会出现在任何的作用域中。它的作用只是用来标识属于相同包的多个文件并在导入时指定默认包名。

标签的作用域

可以使用标签语句来声明标签,并且可以在 breakcontinuegoto 语法中使用。如果只声明但没有使用标签时非法的。标签的作用域只有定义时的函数体,早递归函数体中没有作用。

空标识符

空标识符使用下划线 _ 代表。与一般的非空标识符不同,它作为匿名标识符在声明,运算元和赋值语句中都有特殊含义。

预定义的标识符

以下标识符已经在全局作用域中预先声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Types:
bool byte complex64 complex128 error float32 float64
int int8 int16 int32 int64 rune string
uint uint8 uint16 uint32 uint64 uintptr

Constants:
true false iota

Zero value:
nil

Functions:
append cap close complex copy delete imag len
make new panic print println real recover

导出标识符

标识符可以导出供其他包使用。在以下两种情况同时满足时标识符是导出的:

  • 标识符的首字母是大写(Unicode 的 Lu 类)
  • 标识符声明在包作用域或者它是字段名/方法名。

其他任何标识符都不是导出的。

标识符的唯一性

给定一个标识符集合,一个标识符与集合中的每个标识符都不相同,那就认为这个标识符是唯一的。假设有两个标识符,如果它们的拼写不同,或者它们在不同的包中并没有导出,那它们就是不同标识符。相反,其他情况下都认为标识符是相同的。

常量声明

常量声明使用常量表达式绑定一系列标识符。标识符的数量必须等于表达式的数量。左侧第 n 个标识符绑定右侧第 n 个表达式的值。

1
2
3
4
5
ConstDecl      = "const" ( ConstSpec | "(" { ConstSpec ";" } ")" ) .
ConstSpec = IdentifierList [ [ Type ] "=" ExpressionList ] .

IdentifierList = identifier { "," identifier } .
ExpressionList = Expression { "," Expression } .

如果给定类型,常量会指定类型,并且表达式的值必须能对这个类型进行赋值。

如果没有给定类型。常量会转换成相应的表达式类型。如果表达式的值是无类型常量,那么声明的常量也是无类型的,并且常量的标识符代表常量的值。例如:即使小数部分是 0,只要表达式是浮点数字面值,常量标识符也表示为浮点数常量。

1
2
3
4
5
6
7
8
const Pi float64 = 3.14159265358979323846
const zero = 0.0 // 无类型浮点数常量
const (
size int64 = 1024
eof = -1 // 无类型整型常量
)
const a, b, c = 3, 4, "foo" // a = 3, b = 4, c = "foo", 无类型整型和字符串常量
const u, v float32 = 0, 3 // u = 0.0, v = 3.0

括号内的常量声明列表的表达式除了第一个必须声明其他表达式可以不写。空的表达式列表的值和类型都和前面的非空表达式相同。缺省的表达式列表等价于重复之前的表达式。标识符的数量必须等于表达式的数量。iota常量生成器是一个可以快速生成序列值的机制。

1
2
3
4
5
6
7
8
9
10
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Partyday
numberOfDays // 非导出常量
)

Iota

在常量声明中,预定义的标识符 iota 表示连续的无类型整型常量。它的值为常量声明中每个常量定义的位置(从零开始)。它能够用来生成一个关联常量集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const ( // iota is reset to 0
c0 = iota // c0 == 0
c1 = iota // c1 == 1
c2 = iota // c2 == 2
)

const ( // iota is reset to 0
a = 1 << iota // a == 1
b = 1 << iota // b == 2
c = 3 // c == 3 (没有使用 iota 不过它的值依然递增)
d = 1 << iota // d == 8
)

const ( // iota is reset to 0
u = iota * 42 // u == 0 (无类型整型常量)
v float64 = iota * 42 // v == 42.0 (float64 类型常量)
w = iota * 42 // w == 84 (无类型整型常量)
)

const x = iota // x == 0 (iota 被重置)
const y = iota // y == 0 (iota 被重置)

根据定义,在同一个常量定义中多次使用 iota 会得到相同的值:

1
2
3
4
5
6
const (
bit0, mask0 = 1 << iota, 1<<iota - 1 // bit0 == 1, mask0 == 0 (iota == 0)
bit1, mask1 // bit1 == 2, mask1 == 1 (iota == 1)
_, _ // (iota == 2, unused)
bit3, mask3 // bit3 == 8, mask3 == 7 (iota == 3)
)

最后一个例子利用了最后一个非空表达式列表的隐式重复。

类型声明

类型声明为类型绑定一个标识符。类型声明有2种方式:类型声明和别名声明。

1
2
TypeDecl = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) .
TypeSpec = AliasDecl | TypeDef .
Alias声明

别名声明给指定类型绑定一个标识符名称。

1
AliasDecl = identifier "=" Type .

在标识符作用域内,它作为类型的别名。

1
2
3
4
type (
nodeList = []*Node // nodeList 和 []*Node 是相同类型
Polar = polar // Polar 和 polar 表示相同类型
)
Type 定义

类型定义会创建一个新类型并绑定一个标识符,新类型与给定类型具有相同的底层类型和操作。

1
TypeDef = identifier Type .

这个类型叫做定义类型,它和其他所有类型都不相同,包括创建它的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type (
Point struct{ x, y float64 } // Point 和 struct{ x, y float64 } 是不同类型
polar Point // polar 和 Point 表示不同类型
)

type TreeNode struct {
left, right *TreeNode
value *Comparable
}

type Block interface {
BlockSize() int
Encrypt(src, dst []byte)
Decrypt(src, dst []byte)
}

定义类型可以关联该类型的方法。它不会继承原来类型的任何方法。但是接口类型的方法集和类型的结构没有改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Mutex 是一个拥有 Lock 和 Unlock 两个方法的数据类型。
type Mutex struct { /* Mutex fields */ }
func (m *Mutex) Lock() { /* Lock implementation */ }
func (m *Mutex) Unlock() { /* Unlock implementation */ }

// NewMutex 与 Mutex 结构相同不过方法集为空。
type NewMutex Mutex

// PtrMutex 的底层类型 *Mutex 的方法集没有改变,
// 但是 PtrMutex 的方法集为空。
type PtrMutex *Mutex

// *PrintableMutex 包含嵌入字段 Mutex 的 Lock 和 Unlock 方法。
type PrintableMutex struct {
Mutex
}

// MyBlock 是与 Block 有相同方法集的接口类型
type MyBlock Block

类型定义可以定义方法集不同的布尔值、数字和字符串类型:

1
2
3
4
5
6
7
8
9
10
11
12
type TimeZone int

const (
EST TimeZone = -(5 + iota)
CST
MST
PST
)

func (tz TimeZone) String() string {
return fmt.Sprintf("GMT%+dh", tz)
}

变量声明

变量声明可以创建一个或多个变量,并绑定对应的标识符、指定类型和初始值。

1
2
VarDecl     = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .
1
2
3
4
5
6
7
8
9
10
var i int
var U, V, W float64
var k = 0
var x, y float32 = -1, -2
var (
i int
u, v, s = 2.0, 3.0, "bar"
)
var re, im = complexSqrt(-1)
var _, found = entries[name] // map lookup; only interested in "found"

如果给定一个表达式列表。变量会根据赋值规则使用表达式进行初始化。否则,每个变量都会初始化成变量类型的零值。

如果指定类型,变量会为指定类型。如果没有指定类型,变量会使用分配的初始值类型。如果初始值为无类型常量,它会转换成初始值的默认类型。如果是一个无类型布尔值,那么变量的类型就是 bool。值 nil 不能给没有指定类型的变量赋值。

1
2
3
4
var d = math.Sin(0.5)  // d is float64
var i = 42 // i is int
var t, ok = x.(T) // t is T, ok is bool
var n = nil // illegal

实现的限制:在函数体内声明的变量如果没有使用过编译器需要报错。

短变量声明

短变量声明的语法:

1
ShortVarDecl = IdentifierList ":=" ExpressionList .

它比正常使用初始化表达式进行变量声明的方式要短,而且不指定类型:

1
"var" IdentifierList = ExpressionList .
1
2
3
4
5
i, j := 0, 10
f := func() int { return 7 }
ch := make(chan int)
r, w := os.Pipe(fd) // os.Pipe() 返回两个值
_, y, _ := coord(p) // coord() 返回三个值,我们只关注 y

和常规变量声明不同,即使之前在相同代码块中声明过的变量,也可以在短变量重新声明相同类型的变量,并且保证至少会有一个新的非空变量。总之,只应该在多变量短声明的时候重新声明变量,重新声明并不会使用新的变量,而是给变量分配新值。

1
2
3
field1, offset := nextField(str, 0)
field2, offset := nextField(str, offset) // 重新声明 offset
a, a := 1, 2 // 非法:声明了 a 两次并且没有新的变量

短变量声明只能在函数中使用,例如在 ifforswitch语句的上下文中声明临时变量。

函数声明

函数声明为函数绑定标识符。

1
2
3
FunctionDecl = "func" FunctionName Signature [ FunctionBody ] .
FunctionName = identifier .
FunctionBody = Block .

如果函数指定了返回参数。函数体的语句必须以终止语句结束。

1
2
3
4
5
6
7
8
func IndexRune(s string, r rune) int {
for i, c := range s {
if c == r {
return i
}
}
// 无效:缺少 return 语句
}

函数声明可以没有函数体。这样的声明提供一个函数声明,并由其他外部实现,例如汇编脚本。

1
2
3
4
5
6
7
8
func min(x int, y int) int {
if x < y {
return x
}
return y
}

func flushICache(begin, end uintptr) // 由外部实现

方法声明

方法是一个带接收者的函数,方法声明为方法绑定标识符作为方法名并指定方法对应的接收者类型。

1
2
MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .
Receiver = Parameters .

接收者通过在方法增加一个额外的参数来指定。这个参数必须是一个非可变参数。它的类型必须是 T 或者 T 的指针(可能包含括号)。T 被称作接收者的基础类型;它不能是指针或接口类型,并且只能在同一个包中定义方法。声明后,我们认为方法绑定了基础类型,并且可以通过 T 或 *T 选择器访问方法名。

非空的接收者标识符在方法签名中必须是唯一的。如果接收者的值没有在该方法中使用,那么接收者标识符可以省略。函数和方法的参数也是一样。

对于一个基础类型。绑定的非空的方法名必须是唯一的。如果基础类型是一个结构体,非空的方法名也不能与结构体字段重复。

给定一个Point类型。声明:

1
2
3
4
5
6
7
8
func (p *Point) Length() float64 {
return math.Sqrt(p.x * p.x + p.y * p.y)
}

func (p *Point) Scale(factor float64) {
p.x *= factor
p.y *= factor
}

为类型 *Point绑定了2个方法 LengthScale

方法的类型就是以接收者作为第一个参数的函数类型,例如 Scale 方法:

1
func(p *Point, factor float64)

但是以这种方式声明的函数并不是方法。

表达式

表达式通过针对运算元使用运算符和函数来获取计算值。

运算元

运算元代表表达式中的一个简单的。运算元可以是字面值,非空标识符。或括号表达式。

空标识符只能出现在赋值声明的左侧。

1
2
3
4
Operand     = Literal | OperandName | MethodExpr | "(" Expression ")" .
Literal = BasicLit | CompositeLit | FunctionLit .
BasicLit = int_lit | float_lit | imaginary_lit | rune_lit | string_lit .
OperandName = identifier | QualifiedIdent.

修饰标识符

修饰标识符是以包名作为前缀修饰的标识符。包名和标识符都不能为空。

1
QualifiedIdent = PackageName "." identifier .

修饰标识符可以用来访问不同包(需要先导入)中的标识符。标识符必须是导出的并在包级代码块声明才能够被访问。

1
math.Sin	// 表示 math 包中的 Sin 函数

复合字面值

复合字面值能为结构体、数组、切片和 map 初始化值。它每次只能创建一个值。字面值由一个字面值类型和使用括号括起来的元素列表组成。元素前也可以声明元素对应的键。

1
2
3
4
5
6
7
8
9
CompositeLit  = LiteralType LiteralValue .
LiteralType = StructType | ArrayType | "[" "..." "]" ElementType |
SliceType | MapType | TypeName .
LiteralValue = "{" [ ElementList [ "," ] ] "}" .
ElementList = KeyedElement { "," KeyedElement } .
KeyedElement = [ Key ":" ] Element .
Key = FieldName | Expression | LiteralValue .
FieldName = identifier .
Element = Expression | LiteralValue .

字面值类型的底层类型必须是一个结构体,数组,切片或 map 类型(如果没有指定类型名就会强制执行这个约束)。元素的类型和键都必须能够分配给相应的字段的元素和键类型;没有额外的类型转换。键可以表示结构体的字段名,切片和数组的索引,map 类型的键。对于 map 字面值,所有的元素都必须有键。如果相同字段名或常量值的键对应多个元素就会报错。如果 map 类型的键为非常量类型,请看求值顺序章节。

结构体字面值遵循以下规则:

  • 在结构体中,键必须是它的字段名。

  • 不包含任何键的元素列表的顺序需要与结构体字段的声明顺序相同。

  • 如果一个元素指定了键,那么所有的元素都必须指定键。

  • 包含键的元素列表不需要指定结构体的每个字字段,缺省字段会使用字段类型的零值。

  • 字面值可以不指定元素;这样的字面值等于该类型的零值。

  • 指定非本包的非导出字段会报错。

给定声明:

1
2
type Point3D struct { x, y, z float64 }
type Line struct { p, q Point3D }

我们可以使用这种写法:

1
2
origin := Point3D{}                            // Point3D 的零值
line := Line{origin, Point3D{y: -4, z: 12.3}} // line.q.x 的零值

数组和切片遵循以下规则:

  • 每个元素都关联一个数字索引标记元素再数组中的位置。

  • 给元素指定的键会作为它的索引。键必须是能够表示非负的 int 类型值的常量;如果是指定类型的常量,那么常量必须是整型。

  • 元素没有指定键时会使用之前的索引加一。如果第一个元素没有指定键,它的索引为零。

对复合字面值取址会生成指向由字面量初始化的变量的指针。

1
var pointer *Point3D = &Point3D{y: 1000}

数组字面值需要在类型中指定数组的长度。如果提供的元素少于数组的长度,那么缺少元素的位置将会使用元素类型的零值替代。如果索引超过数组的长度会报错。 表示数组的长度等于最大元素索引加一。

1
2
3
buffer := [10]string{}             // len(buffer) == 10
intSet := [6]int{1, 2, 3, 5} // len(intSet) == 6
days := [...]string{"Sat", "Sun"} // len(days) == 2

切片字面值底层其实就是数组字面值。因此它的长度和容量都是元素的最大索引加一。切片字面值的格式为:

1
[]T{x1, x2, … xn}

可以在数组上进行切片操作从而获得切片:

1
2
tmp := [n]T{x1, x2, … xn}
tmp[0 : n]

在一个数组、切片或 map 类型 T 中。元素或者 map 的键可能有自己的字面值类型,如果字面值类型和元素或者键类型相同,那么对应的类型标识符可以省略。与之类似,如果元素或键的类型为 *T,那么它们的 &T 也可以省略。

1
2
3
4
5
6
7
8
9
[...]Point{{1.5, -3.5}, {0, 0}}     // same as [...]Point{Point{1.5, -3.5}, Point{0, 0}}
[][]int{{1, 2, 3}, {4, 5}} // same as [][]int{[]int{1, 2, 3}, []int{4, 5}}
[][]Point{{{0, 1}, {1, 2}}} // same as [][]Point{[]Point{Point{0, 1}, Point{1, 2}}}
map[string]Point{"orig": {0, 0}} // same as map[string]Point{"orig": Point{0, 0}}
map[Point]string{{0, 0}: "orig"} // same as map[Point]string{Point{0, 0}: "orig"}

type PPoint *Point
[2]*Point{{1.5, -3.5}, {}} // same as [2]*Point{&Point{1.5, -3.5}, &Point{}}
[2]PPoint{{1.5, -3.5}, {}} // same as [2]PPoint{PPoint(&Point{1.5, -3.5}), PPoint(&Point{})}

当复合字面值使用字面值类型的类型名格式出现在 ifforswitch 语句的关键字和括号之间并且没有使用圆括号包裹的时候,会引发语法歧义。在这种特殊的情况下字面值的括号会被认为是语句的代码块。为了避免歧义,复合字面值必须用括号括起来。

1
2
if x == (T{a,b,c}[i]) { … }
if (x == T{a,b,c}[i]) { … }

下面是合法的数组、切片和 map 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// list of prime numbers
primes := []int{2, 3, 5, 7, 9, 2147483647}

// vowels[ch] is true if ch is a vowel
vowels := [128]bool{'a': true, 'e': true, 'i': true, 'o': true, 'u': true, 'y': true}

// the array [10]float32{-1, 0, 0, 0, -0.1, -0.1, 0, 0, 0, -1}
filter := [10]float32{-1, 4: -0.1, -0.1, 9: -1}

// frequencies in Hz for equal-tempered scale (A4 = 440Hz)
noteFrequency := map[string]float32{
"C0": 16.35, "D0": 18.35, "E0": 20.60, "F0": 21.83,
"G0": 24.50, "A0": 27.50, "B0": 30.87,
}

函数字面值

函数字面值表示一个匿名函数。

1
FunctionLit = "func" Function .
1
func(a, b int, z float64) bool { return a*b < int(z) }

函数字面值能分配给变量或直接调用。

函数字面值是一个闭包。它可以引用包裹函数中的变量,这些变量在包裹函数和函数字面值之间是共享的。并且它会一直存在直到生命周期结束。

主要表达式

主要表达式是一元和二元表达式的运算元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PrimaryExpr =
Operand |
Conversion |
PrimaryExpr Selector |
PrimaryExpr Index |
PrimaryExpr Slice |
PrimaryExpr TypeAssertion |
PrimaryExpr Arguments .

Selector = "." identifier .
Index = "[" Expression "]" .
Slice = "[" [ Expression ] ":" [ Expression ] "]" |
"[" [ Expression ] ":" Expression ":" Expression "]" .
TypeAssertion = "." "(" Type ")" .
Arguments = "(" [ ( ExpressionList | Type [ "," ExpressionList ] ) [ "..." ] [ "," ] ] ")" .
1
2
3
4
5
6
7
8
9
x
2
(s + ".txt")
f(3.1415, true)
Point{1, 2}
m["foo"]
s[i : j + 1]
obj.color
f.p[i].x()

选择器

对于一个 x 不是包名的主要表达式,选择器表达式:

1
x.f

表示 x 的字段或方法 f(有时为 *x)。标识符 f 叫做(字段/方法)选择器。它不能是空标识符。选择器表达式的类型就是 f 的类型。如果 x 是包名。请参考修饰标识符。

选择器 f 可以表示类型 T 的方法或字段 f。也可以表示类型 T 的嵌入方法或字段 f。访问 f 所需穿过的嵌套层数叫做它在类型 T 中的深度。声明在 T 中的字段或方法的深度为 0。声明在 T 的嵌入字段 A 中的方法或字段的深度等于 f 在 A 中的深度加一。

选择器遵循以下原则:

  • 对于非指针/接口类型 T/*T 的值 x,x.f 表示第一层的方法/字段。如果在第一层没有对应的 f,选择器表达式就是非法的。

  • 对于接口类型 I 的值 x,x.f表示动态值 x 的方法名 f。如果接口 I 的方法集中没有 f 方法,选择器就是非法的。

  • 作为例外,如果 x 是一个指针类型并且 (*x).f 是合法的选择器表达式(只能表示字段,不能表示方法)。那么(*x).f 可以简写成 x.f。

  • 在其他情况下,x.f 都是非法的。

  • 如果x是指针类型,并且值为 nil,其中 f 为结构体字段。赋值或取值 x.f 会引起运行时恐慌。

  • 如果x是接口类型,并且值为 nil。调用 x.f 会引起运行时恐慌。

例如给定声明:

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 T0 struct {
x int
}

func (*T0) M0()

type T1 struct {
y int
}

func (T1) M1()

type T2 struct {
z int
T1
*T0
}

func (*T2) M2()

type Q *T2

var t T2 // with t.T0 != nil
var p *T2 // with p != nil and (*p).T0 != nil
var q Q = p

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
t.z          // t.z
t.y // t.T1.y
t.x // (*t.T0).x

p.z // (*p).z
p.y // (*p).T1.y
p.x // (*(*p).T0).x

q.x // (*(*q).T0).x (*q).x is a valid field selector

p.M0() // ((*p).T0).M0() M0 expects *T0 receiver
p.M1() // ((*p).T1).M1() M1 expects T1 receiver
p.M2() // p.M2() M2 expects *T2 receiver
t.M2() // (&t).M2() M2 expects *T2 receiver, see section on Calls

但是下面这种方式是不合法的:

1
q.M0()       // (*q).M0 is valid but not a field selector

方法表达式

如果 M 在类型 T 的方法集中。那么 T.M 就是能够正常调用的函数。使用与 M 相同的参数只是在参数列表的最前面增加了接收者参数。

1
2
MethodExpr    = ReceiverType "." MethodName .
ReceiverType = TypeName | "(" "*" TypeName ")" | "(" ReceiverType ")" .

假设结构体 T 有两个方法。接收者类型为 T 的 Mv 方法和接收者类型为 *T 的 Mp 方法:

1
2
3
4
5
6
7
type T struct {
a int
}
func (tv T) Mv(a int) int { return 0 } // value receiver
func (tp *T) Mp(f float32) float32 { return 1 } // pointer receiver

var t T

表达式

1
T.Mv

将会生成一个等价于 Mv 方法只是第一个参数显式声明接受者的函数。它的签名为:

1
func(tv T, a int) int

这个函数能够通过接收者正常调用,以下5种方式是等价的:

1
2
3
4
5
t.Mv(7)
T.Mv(t, 7)
(T).Mv(t, 7)
f1 := T.Mv; f1(t, 7)
f2 := (T).Mv; f2(t, 7)

与之类似:

1
(*T).Mp

生成表示 Mp 的函数签名:

1
func(tp *T, f float32) float32

对于一个把值作为接收者的方法,我们可以显式的从指针接收者获得函数:

1
(*T).Mv

生成表示 Mv 的函数签名:

1
func(tv *T, a int) int

这样的函数会通过接收者间接的创建一个值作为接收者传入底层方法中。方法内不能修改接收者的值,因为它的地址是在函数的调用栈里面。

最后一个例子。把值作为接收者函数当做指针作为接收者的方法是非法的,因为指针接收者的方法集中不包含值类型的方法集。

通过函数调用语法从方法中获取函数的值。接收者作为调用函数的第一个参数。给定 f :=T.Mv,f 作为f(t,7) 进行调用而不是 t.f(7)。想创建一个绑定接收者的函数可以使用函数字面值或者方法值。

在接口类型中定义函数获取函数值是合法的。最终的函数调用会使用接口类型作为接收者。

方法值

如果表达式 x 拥有静态类型 T 并且 M 在类型 T 的方法集中。x.M 叫做方法值。方法值 x.M 是一个函数值,这个函数和 x.M 拥有相同的参数列表。表达式 x 在计算方法值时会被保存和计算,这个拷贝的副本会作为任何接下来调用的接收者。

类型 T 可能是接口类型也可能不是接口类型。

与方法表达式中讲过的一样,假设类型 T 有两个方法:接收者类型为 T 的 Mv 和接受者类型为 *T 的 Mp :

1
2
3
4
5
6
7
8
9
type T struct {
a int
}
func (tv T) Mv(a int) int { return 0 } // value receiver
func (tp *T) Mp(f float32) float32 { return 1 } // pointer receiver

var t T
var pt *T
func makeT() T

表达式:

1
t.Mv

生成一个类型的函数值:

1
func(int) int

以下两种调用是等价的:

1
2
t.Mv(7)
f := t.Mv; f(7)

相似的,表达式:

1
pt.Mp

生成一个类型的函数值:

1
func(float32) float32

与选择器相同,使用指针调用以值作为接收者的非接口方法会自动将指针解引用:pt.Mv 等价于 (*pt).Mv

与方法调用相同,使用值调用以指针作为接收者的非接口方法会自动对值取址:pt.Mv 等价于 (&pt).Mv

1
2
3
4
5
f := t.Mv; f(7)   // like t.Mv(7)
f := pt.Mp; f(7) // like pt.Mp(7)
f := pt.Mv; f(7) // like (*pt).Mv(7)
f := t.Mp; f(7) // like (&t).Mp(7)
f := makeT().Mp // invalid: result of makeT() is not addressable

尽管上面使用的都是非接口类型的例子,不过对于接口类型同样适用。

1
2
var i interface { M(int) } = myVal
f := i.M; f(7) // like i.M(7)

index表达式

主要表达式格式:

1
a[x]

可以表示数组元素、数组的指针、切片、字符串或 map 类型 a 索引 x 对应的值。x 称作索引或者 map 的键。遵循以下规则:

如果a不是 map 类型:

  • 索引 x 必须是整型或无类型常量。

  • 常量索引必须是非负数且可以使用 int 类型表示。

  • 无类型的常量索引会作为 int 型的值。

  • 索引 x 的范围在 0<=x<len(a) 内,否则就是越界。

对于数组类型 A:

  • 常量索引必须在合法范围内。

  • 如果 x 在运行时越界会引起运行时恐慌。

  • a[x] 表示数组在索引 x 处的元素。a[x] 的类型就是 A 的元素类型。

对于数组的指针类型:

  • 可以使用 a[x] 表示 (*a)[x]

对于切片类型 S:

  • 如果 x 在运行时越界会引起运行时恐慌。
  • a[x] 表示切片在索引 x 处的元素。a[x] 的类型就是 S 的元素类型。

对于字符串类型:

  • 如果字符串 a 为常量,那么常量索引必须在合法范围内。

  • 如果 x 在运行时越界会引起运行时恐慌。

  • a[x] 表示索引 x 处的非常量字节,它是byte类型。

  • 不能对 a[x] 分配值。

对于 map 类型 M:

  • 必须保证 x 的类型能够给 M 的键分配值。

  • 如果map包含键为 x 的值,a[x] 就是 map 中键 x 对应的值,它的类型就是 M 的元素类型。

  • 如果 map 值为 nil 或不包含这个实体,那么 a[x] 为 M 元素类型的零值。

否则 a[x] 就是非法的。

基于 map[K]V 类型 a 的索引表达式可以使用特殊格式的赋值和初始化语法。

1
2
3
v, ok = a[x]
v, ok := a[x]
var v, ok = a[x]

它会额外生成一个无类型的布尔值。如果 ok 是 true,那么代表在map中有该键,如果没有 ok 为 false。

给一个值为 nil 的 map 类型变量赋值会导致运行时恐慌。

切片表达式

切片表达式可以基于字符串、数组、数组指针、切片创建字符串子串或切片。它有两种变体,一种是简单的格式是指定开始和结束位置,完全格式的语法还可以指定容量。

####### 简单切片表达式

对于数组、字符串、指针数组、切片 a,主要表达式:

1
a[low:high]

可以构造字符串子串或切片。索引 lowhigh 决定结果切片中的元素。结果切片的索引从 0 开始,长度为 high - low。从数组切分出的切片 s 拥有类型 []int,长度为 3 ,容积为 4。

1
2
a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]
1
2
3
s[0] == 2
s[1] == 3
s[2] == 4

为了方便起见,索引值都可以缺省。当 low 缺省时默认从 0 开始。当缺 high 缺省时默认的取切片的长度。

1
2
3
a[2:]  // same as a[2 : len(a)]
a[:3] // same as a[0 : 3]
a[:] // same as a[0 : len(a)]

如果 a 是一个数组指针,那么 a[low:high] 可以表示 (*a)[low : high]

对于数组或者字符串,索引的范围是0<=low<=high<=len(a)。对于切片,最大的索引值可以为切片的容量,而不是切片的长度。常量索引必须为非负数,且能够转换成 int 类型。对于数组或者常量字符串。常量索引值必须在合法范围内。如果2个索引都是常量。low 必须小于 high。如果索引在运行时访问了非法内存,程序会发生运行时恐慌。

除了无类型字符串,对于切片和字符串的操作结果是非常量类型的值,它的类型与运算元相同。如果运算元为无类型字符串,那么结果类型会为 string。如果把数组作为运算元,它必须是可寻址的,并且获得的切片和原数组具有同一元素类型。

如果切片运算元为 nil,那么结果也是 nil。否则结果切片会和运算元共享相同的底层无类型数组。

完全切片表达式

对于数组,数组指针或非字符串切片,主要表达式为:

1
a[low : high : max]

它会构造一个同类型切片,并具有与简单切片表达式的 a[low:high] 相同的长度和元素。另外,它还可以把切片的容量设置为 max - low。这时只有第一个索引可以为缺省值,默认为零。从数组中获得切片以后:

1
2
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]

切片 t 为 []int 类型,长度为 2,容量为 4,并且元素为:

1
2
t[0] == 2
t[1] == 3

和简单切片表达式一样,如果 a 是数组指针 ,那么 a[low:high:max] 可以简写为 (*a)[low:high:max]。如果切分操作元是数组,那么这个数组必须是可以寻址的。

如果索引必须在 0 <= low <= high <= max <= cap(a) 范围内。常量索引不能是负数并且能够使用 int 类型表示;对于数组,索引必须在合法范围内。如果有多个索引都是常量的,那么所有索引都需要在合法范围内。如果索引是非法的,会引起运行时恐慌。

类型断言

对于接口类型 x 和类型 T,主要表达式:

1
x.(T)

可以断言 x 不是 nil 且 x 的值是 T 类型。标记 x.(T) 叫做类型断言。

更确切的说,如果 T 不是接口类型,那么 x.(T) 将会断言动态类型 x 的类型是不是 T。

这时,T 必须实现了 x 的(接口)类型。否则断言会是非法的因为 x 不能保存 T 类型的值。如果 T 是接口类型,那么可以断言动态类型 x 是否实现了 T 接口。

如果类型断言成功,表达式的值为 x 的值,但它的类型是T。如果断言失败,将会导致运行时恐慌。换句话说,即使 x 是运行时确定的,x.(T) 也必须是编程时就确认存在的。

1
2
3
4
5
6
7
8
9
10
var x interface{} = 7          // x 拥有动态类型 int 值为 7
i := x.(int) // i 为 int 类型值为 7

type I interface { m() }

func f(y I) {
s := y.(string) // 非法: 字符串没有实现接口 I (缺少 m 方法)
r := y.(io.Reader) // r 拥有接口 io.Reader 所以 y 的动态类型必须同时实现 I 和 io.Reader

}

类型断言可以使用特定格式的赋值和初始化语句。

1
2
3
4
v, ok = x.(T)
v, ok := x.(T)
var v, ok = x.(T)
var v, ok T1 = x.(T)

这时将会额外生成一个无类型的布尔值。如果断言成功,ok返回 true,否则是 false。并且 v 会是 T 类型的零值。这时不会有恐慌发生。

调用

给定函数类型为 F 的表达式 f:

1
f(a1, a2, … an)

可以使用 a1,a2…an 来调用函数 f。除一种特殊情况之外,函数参数必须是对应 F 函数参数类型的单值表达式,且在函数调用前就已经完成求值。表达式的结果类型是 f 的结果类型。函数调用和方法调用相似,只是方法额外需要一个接收者类型。

1
2
3
math.Atan2(x, y)  // function call
var pt *Point
pt.Scale(3.5) // method call with receiver pt

在函数调用中,函数的值和参数是按照顺序求值的。在计算之后作为参数会传进函数,函数开始执行。当函数执行完成后返回的参数将会返回给函数的调用者。

调用值为 nil 的函数会导致运行时恐慌。

作为特例,如果函数或者方法的返回值等于参数列表的个数,那么会嵌套调用。这将把返回值直接赋值给下一次调用函数的参数。

1
2
3
4
5
6
7
8
9
10
11
func Split(s string, pos int) (string, string) {
return s[0:pos], s[pos:]
}

func Join(s, t string) string {
return s + t
}

if Join(Split(value, len(value)/2)) != value {
log.Panic("test fails")
}

如果 x 的方法集中包含 m 那么 x.m() 是合法的。并且参数列表和 m 的参数列表相同。如果x是可寻址的,那么那么x指针的方法集(&x).m()可以简写成x.m()

1
2
var p Point
p.Scale(3.5)

没有方法类型,也没有方法字面值。

通过 ... 来传递参数

如果 f 的最后一个参数 p 的类型是 ...T。那么在函数内部 p 参数的类型就是 []T。如果 f 调用时没有传入 p 对应的参数,那么p为 nil。否则这些参数会以切片方式传入,在新的底层切片中。切片中的类型都是能赋值给类型 T 的值。这个切片的长度和容量在不同的调用中有所不同。

给定函数调用:

1
2
3
func Greeting(prefix string, who ...string)
Greeting("nobody")
Greeting("hello:", "Joe", "Anna", "Eileen")

Greeting 中,第一次调用时,who是 nil 类型。而在第二次调用时是[]string{"Joe", "Anna", "Eileen"}

如果在调用的时候的最后一个参数是[]T,那么我们可以使用...来将切片中的值依次赋值给参数列表。

给定切片s并且调用:

1
2
s := []string{"James", "Jasmine"}
Greeting("goodbye:", s...)

z 在 Greeting。中 who 会和切片 s 共享相同的底层数组。

操作符

操作符用来连接运算元。

1
2
3
4
5
6
7
8
9
Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr = PrimaryExpr | unary_op UnaryExpr .

binary_op = "||" | "&&" | rel_op | add_op | mul_op .
rel_op = "==" | "!=" | "<" | "<=" | ">" | ">=" .
add_op = "+" | "-" | "|" | "^" .
mul_op = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .

unary_op = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .

比较运算符在此处讨论。对于其他二元操作符,两个操作元的类型必须是相同的,除了位移和无类型常量。针对常量的操作,请看常量表达式章节。

除了位移操作,如果其中一个操作符是无类型常量,而另个不是,那么无类型的常量会转换成另一个运算元的类型。

在右移表达式中的运算元必须是无符号的整数或者可以转换成 uint 的无类型的常量。如果左移一个无类型常量那么结果依然是无类型的。他首先会转换成指定类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
var s uint = 33
var i = 1<<s // 1 has type int
var j int32 = 1<<s // 1 has type int32; j == 0
var k = uint64(1<<s) // 1 has type uint64; k == 1<<33
var m int = 1.0<<s // 1.0 has type int; m == 0 if ints are 32bits in size
var n = 1.0<<s == j // 1.0 has type int32; n == true
var o = 1<<s == 2<<s // 1 and 2 have type int; o == true if ints are 32bits in size
var p = 1<<s == 1<<33 // illegal if ints are 32bits in size: 1 has type int, but 1<<33 overflows int
var u = 1.0<<s // illegal: 1.0 has type float64, cannot shift
var u1 = 1.0<<s != 0 // illegal: 1.0 has type float64, cannot shift
var u2 = 1<<s != 1.0 // illegal: 1 has type float64, cannot shift
var v float32 = 1<<s // illegal: 1 has type float32, cannot shift
var w int64 = 1.0<<33 // 1.0<<33 is a constant shift expression
运算符优先级

一元运算符拥有最高优先级。++ 和 – 是语句而不是表达式,他们在运算符的优先级之外。所以 (*p)++ 和 *p++ 是一样的。

二元运算符有 5 个优先级。乘法运算符在最高级,紧接着是加法运算符。比较运算符,&& 运算符,最后是 ||。

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

相同优先级的二元运算符的执行顺序是由左到右。例如 x/y*z(x/y)*z 是一样的。

1
2
3
4
5
6
+x
23 + 3*x[i]
x <= f()
^a >> b
f() || g()
x == y+1 && <-chanPtr > 0

算数运算符

算数运算符应用在 2 个数字值之间,别切生成一个相同类型的值作为第一个运算元。四种算数运算符(+,-,*,/)应用在数字,浮点,复合类型之中。+ 也可以用于字符串。位运算和位移运算只适用于整数。

1
2
3
4
5
6
7
8
9
10
11
12
13
+    sum                    integers, floats, complex values, strings
- difference integers, floats, complex values
* product integers, floats, complex values
/ quotient integers, floats, complex values
% remainder integers

& bitwise AND integers
| bitwise OR integers
^ bitwise XOR integers
&^ bit clear (AND NOT) integers

<< left shift integer << unsigned integer
>> right shift integer >> unsigned integer
数字运算符

对于两个整数 x 和 y。整数商 q=x/y 和余数 r=x%y 遵循以下规律。

1
x = q*y + r  and  |r| < |y|

x/y 截断为 0。

1
2
3
4
5
x     y     x / y     x % y
5 3 1 2
-5 3 -1 -2
5 -3 -1 2
-5 -3 1 -2

作为这个规则的例外情况,如果 x 非常大,那么 q=x/-1 等于 x。

1
2
3
4
5
x, q
int8 -128
int16 -32768
int32 -2147483648
int64 -9223372036854775808

如果除数是一个常量。那么它不能是 0,如果除数在运行时为 0,会导致运行时恐慌。如果除数是负数并且除数是:

1
2
3
x     x / 4     x % 4     x >> 2     x & 3
11 2 3 2 3
-11 -2 -3 -3 1

位移运算符移动左侧运算元右侧元算元指定的位数。如果左侧是有符号整型,那它就实现了位移运算,如果是无符号整数使用逻辑位移。位移运算没有上限,位移操作让左边运算元位移 n 个 1。x<<1x*2 是相等的。并且 x>>1x/2 是相同的。

对于整数运算元,一元运算符+-^定义如下:

1
2
3
4
+x                          is 0 + x
-x negation is 0 - x
^x bitwise complement is m ^ x with m = "all bits set to 1" for unsigned x
and m = -1 for signed x
整型溢出

对于无符号的值,运算符+-*和<<都是2禁止运算。这里的n是无符号类型的宽度,无符号整型将会丢弃溢出的位,并且程序将会返回wrap around

对于有符号的整数,操作符+=*<<都会溢出并且值存在,并且代表相应的有符号的值。在运算时不会抛出异常。标一起不会报错。所以不是所有情况下x<x+1都成立。

浮点数运算符

对于浮点数和其他复杂数字,+x和x是一样的,-x是x的对立面。除了IEEE-754还没有指定浮点数除0或者复数的结果。是否抛出异常将会依赖其具体实现。

一种实现可以合并多个浮点操作进一个操作,有可能是夸语句的,并且他的结果可能和依次单独执行的结果不一样。1个浮点数类型将会转变成目标的精度,防止四舍五入的融合。

1
2
3
4
5
6
7
8
9
10
11
// FMA allowed for computing r, because x*y is not explicitly rounded:
r = x*y + z
r = z; r += x*y
t = x*y; r = t + z
*p = x*y; r = *p + z
r = x*y + float64(z)

// FMA disallowed for computing r, because it would omit rounding of x*y:
r = float64(x*y) + z
r = z; r += float64(x*y)
t = float64(x*y); r = t + z
字符串

字符串可以使用+和+=操作符。

1
2
s := "hi" + string(c)
s += " and good bye"

字符串想家将会创建一个新的字符串。

比较运算符

比较运算符比较连个运算元,并且生成一个无类型的布尔值。

1
2
3
4
5
6
==    equal
!= not equal
< less
<= less or equal
> greater
>= greater or equal

在任何比较运算元中2种类型必须是可以分配的。

使用等于运算符==!=的运算元必须是可比较的。使用顺序运算符<,<=,>>=必须是可比较的。这些限制导致比较运算符被定义成以下的方式。

  • 布尔值是可比较的,两个布尔值当他们同为true或者false的使用是相等的

  • 整数值是可比较和排序的

  • 浮点数是可比较和排序的,具体定义在IEEE-754标准中。

  • 复数是可比较的,2个复数当实部和虚部都相等时就是相等的。

  • 字符串是可以比较和排序的。是按照字节顺序排序。

  • 指针式可以排序的,连个指针当指向相同变量时是相同的,或者他们2个都是nil。指向一个为非配的变量的结果是未定义的。

  • channel是可比较的。当两个管道是用同一个make出来的,或者都是nil时时相等的。

  • 接口值时可以比较的,2个接口值时相等的如果2个标识符的动态类型是一样的或者他们都是nil。

  • 一个非接口类型的值x和一个接口类型的值T在非接口类型是可以比较的并且非接口类型实现了接口是是可以比较的。当他们的动态类型类型相同时时相等的。

  • 当结构体内的所有字段都是可以比较的时候,他是可以比较的。连个结构体的值当非空字段都相等时他们是相等的。

  • 数组类型的值时可比较的,如果数组的原属时可以比较的,那么当数组的所有值是相等的时候他们就是相等的。

使用两个动态类型的标识符来比较接口的值。如果这个类型的值时不可比较的,那么将会引起一个panic。这个行为不仅仅时接口,数组结构体接口字段都有这个问题。

切片,map,和函数值都是不可比较的,然而,作为一个特殊的例子,切片,map和函数的值的nil时可以比较的,指针,channel和接口的值nil也是可以比较的。

1
2
3
4
5
6
7
8
9
10
11
const c = 3 < 4            // c is the untyped boolean constant true

type MyBool bool
var x, y int
var (
// The result of a comparison is an untyped boolean.
// The usual assignment rules apply.
b3 = x == y // b3 has type bool
b4 bool = x == y // b4 has type bool
b5 MyBool = x == y // b5 has type MyBool
)

逻辑操作符

逻辑运算符使用布尔值值,并且生成一个相同类型的结果值作为操作元。右面的操作元计算是有条件的。

1
2
3
&&    conditional AND    p && q  is  "if p then q else false"
|| conditional OR p || q is "if p then true else q"
! NOT !p is "not p"

地址操作符

以类型 T 的 x 作为运算元,取址操作 &x 会生成一个类型为 *T 并指向 x 的指针。运算元必须是能够取址的,它可以是一个变量,指针,切片的取值操作;或是一个可取址结构体的字段选择器;或是对于可取址数组的索引取值操作。作为寻址能力的例外,x 可能是一个复合字面值。如果对 x 进行取址操作将会 panic,&x 也会 panic。

对于一个 *T 类型的运算元 x,指针解引用 *x 表示 x 指向的 T 类型。如果 x 为 nil,那么解引用 *x 会 panic。

1
2
3
4
5
6
7
8
9
&x
&a[f(2)]
&Point{2, 3}
*p
*pf(x)

var x *int = nil
*x // causes a run-time panic
&*x // causes a run-time panic

接收操作符

对于管道类型的运算元 ch,接收操作 <-ch 返回值是管道 ch 接收到的值。带方向的管道需要有接受权限,接收操作的类型也是通道的元素类型。表达式会一直阻塞直到接收到返回值。从 nil 通道接收值会一直阻塞。从一个已经关闭的通道接收数据会在其他数据都被接收以后生成该通道元素类型的零值。

1
2
3
4
v1 := <-ch
v2 = <-ch
f(<-ch)
<-strobe // wait until clock pulse and discard received value

接收数据的表达式可以使用赋值表达式。

1
2
3
4
x, ok = <-ch
x, ok := <-ch
var x, ok = <-ch
var x, ok T = <-ch

它还可以生成一个额外的无类型布尔值来表示通道是否关闭。如果 ok 为 true 说明获取到的是发送到通道内的数据,而 false 它就返回一个零值因为通道内没有元素且已经关闭。

类型转换

类型转换表达式 T(x) 其中 T 代表类型,x 代表可以转换成 T 类型的表达式。

1
Conversion = Type "(" Expression [ "," ] ")" .

如果类型是以 *<- 开头,或以关键字 func 开头并且没有返回值列表,那么它必须用括号括起来避免歧义:

1
2
3
4
5
6
7
8
*Point(p)        // same as *(Point(p))
(*Point)(p) // p is converted to *Point
<-chan int(c) // same as <-(chan int(c))
(<-chan int)(c) // c is converted to <-chan int
func()(x) // function signature func() x
(func())(x) // x is converted to func()
(func() int)(x) // x is converted to func() int
func() int(x) // x is converted to func() int (unambiguous)

常量 x 可以在可以用类型 T 表示时自动转换。作为一个特例,整数常量 x 可以转换成字符串类型就和非常量 x 一样。

对常量的转换会生成一个指定类型的常量。

1
2
3
4
5
6
7
8
9
10
11
12
uint(iota)               // iota value of type uint
float32(2.718281828) // 2.718281828 of type float32
complex128(1) // 1.0 + 0.0i of type complex128
float32(0.49999999) // 0.5 of type float32
float64(-1e-1000) // 0.0 of type float64
string('x') // "x" of type string
string(0x266c) // "♬" of type string
MyString("foo" + "bar") // "foobar" of type MyString
string([]byte{'a'}) // not a constant: []byte{'a'} is not a constant
(*int)(nil) // not a constant: nil is not a constant, *int is not a boolean, numeric, or string type
int(1.2) // illegal: 1.2 cannot be represented as an int
string(65.0) // illegal: 65.0 is not an integer constant

非常量 x 可以在以下情况下转换成类型 T:

  • x 可以给类型 T 赋值

  • 忽略的结构体标签,x 的类型和 T 具有相同的底层类型

  • 忽略的结构体标签,x 的类型和 T 都是指针类型,并且指针所指的类型具有相同的底层类型

  • x 的类型和 T 都是整数或者浮点数类型

  • x 的类型和 T 都是复数类型

  • x 是一个字符串而 T 时字节切片或者 rune 切片

在比较两个结构体类型的时候会忽略结构体标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Person struct {
Name string
Address *struct {
Street string
City string
}
}

var data *struct {
Name string `json:"name"`
Address *struct {
Street string `json:"street"`
City string `json:"city"`
} `json:"address"`
}

var person = (*Person)(data) // ignoring tags, the underlying types are identical

这个规则也适用于数字类型与字符串类型间的相互转换。这个转换可能会改变 x 的值并且会增加运行时消耗。包 unsafe 实现了这个功能底层的限制。

数字之间的转换

对于非常量的数字转换,需要遵守以下规则:

  • 在转换整型数字时,如果是一个有符号整型,它是继承有符号的无限精度;否则就不用继承符号。转换时会截断数字以适应类型的大小。例如:如果 v:=uint16(0x10F0),然后 ``uint32(int8(v)) == 0xFFFFFFF0 。类型转换总是生成有效值,并且永远不会溢出。

  • 如果要将浮点数转换成整型,会丢弃小数部分(截断为零)。

  • 如果要将整型或浮点型转换成浮点数类型,或或者一个复数转换成其他复数类型,结果会四舍五入成指定精度。例如: 可以使用超出IEEE-754 32位数的附加精度来存储float32类型的变量x的值,但float32(x)表示将x的值舍入为32位精度的结果。x + 0.1 会使用超过 32 位的精度,而 float32(x+0.1) 不会。

在所有浮点数和复数的非常量转换中,如果结构类型不能成功表示数据,那么结果将会依赖于具体平台实现。

字符串的类型转换
  1. 转换一个有符号或者无符号的整型值会转换成对应的 UTF-8 表示整型值。不在范围内的 Unicode 代码点会转换成 “\uFFFD”。
1
2
3
4
5
string('a')       // "a"
string(-1) // "\ufffd" == "\xef\xbf\xbd"
string(0xf8) // "\u00f8" == "ø" == "\xc3\xb8"
type MyString string
MyString(0x65e5) // "\u65e5" == "日" == "\xe6\x97\xa5"
  1. 将字节切片转换成字符串类型会生成一个由切片元素组成的字符串
1
2
3
4
5
6
string([]byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'})   // "hellø"
string([]byte{}) // ""
string([]byte(nil)) // ""

type MyBytes []byte
string(MyBytes{'h', 'e', 'l', 'l', '\xc3', '\xb8'}) // "hellø"
  1. 将 rune 切片转换成字符串类型会生成一个由切片元素组成的字符串
1
2
3
4
5
6
string([]rune{0x767d, 0x9d6c, 0x7fd4})   // "\u767d\u9d6c\u7fd4" == "白鵬翔"
string([]rune{}) // ""
string([]rune(nil)) // ""

type MyRunes []rune
string(MyRunes{0x767d, 0x9d6c, 0x7fd4}) // "\u767d\u9d6c\u7fd4" == "白鵬翔"
  1. 将字符串转换成字节切片会生成由字符串中每个字节组成的切片
1
2
3
4
[]byte("hellø")   // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}
[]byte("") // []byte{}

MyBytes("hellø") // []byte{'h', 'e', 'l', 'l', '\xc3', '\xb8'}
  1. 将字符串转换成 rune 切片会生成由字符串中每个 Unicode 代码点组成的切片
1
2
3
4
[]rune(MyString("白鵬翔"))  // []rune{0x767d, 0x9d6c, 0x7fd4}
[]rune("") // []rune{}

MyRunes("白鵬翔") // []rune{0x767d, 0x9d6c, 0x7fd4}
常量表达式

常量表达式只包含常量运算元并且在编译程序时就已经计算完成。

无类型布尔值,数值和字符串常量都可以当作运算元。除了位置操作符,如果二元运算符石不同类型的常量,操作元,和非布尔值,和即将在接下来出现的:整型,rune,浮点数和复数类型。例如:一个无类型整型常量减去无类型复数常量,结果为复数常量。

一个常量的比较运算会生成无类型的布尔常量。如果左移运算是一个无类型常量,结果会是一个整型常量。它会和原来常量为相同类型。其他与无类型常量的运算都会生成相同类型的结果(布尔值,整型,浮点数,复数,字符串常量)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const a = 2 + 3.0          // a == 5.0   (untyped floating-point constant)
const b = 15 / 4 // b == 3 (untyped integer constant)
const c = 15 / 4.0 // c == 3.75 (untyped floating-point constant)
const Θ float64 = 3/2 // Θ == 1.0 (type float64, 3/2 is integer division)
const Π float64 = 3/2. // Π == 1.5 (type float64, 3/2. is float division)
const d = 1 << 3.0 // d == 8 (untyped integer constant)
const e = 1.0 << 3 // e == 8 (untyped integer constant)
const f = int32(1) << 33 // illegal (constant 8589934592 overflows int32)
const g = float64(2) >> 1 // illegal (float64(2) is a typed floating-point constant)
const h = "foo" > "bar" // h == true (untyped boolean constant)
const j = true // j == true (untyped boolean constant)
const k = 'w' + 1 // k == 'x' (untyped rune constant)
const l = "hi" // l == "hi" (untyped string constant)
const m = string(k) // m == "x" (type string)
const Σ = 1 - 0.707i // (untyped complex constant)
const Δ = Σ + 2.0e-4 // (untyped complex constant)
const Φ = iota*1i - 1/1i // (untyped complex constant)

对一个无类型整数,rune,或浮点数应用内置的 complex 函数会生成无类型的复数常量。

1
2
const ic = complex(0, c)   // ic == 3.75i  (untyped complex constant)
const iΘ = complex(0, Θ) // iΘ == 1i (type complex128)

常量表达式总是一个明确的值;中间值和常量自己可以比语言所支持的精度更高,下面的声明是合法的:

1
2
const Huge = 1 << 100         // Huge == 1267650600228229401496703205376  (untyped integer constant)
const Four int8 = Huge >> 98 // Four == 4 (type int8)

常量的除法的除数不能为 0:

1
3.14 / 0.0   // illegal: division by zero

定义了类型的常量的精度必须根据常量类型定义。所以下面的常量表达式是非法的:

1
2
3
4
5
uint(-1)     // -1 cannot be represented as a uint
int(3.14) // 3.14 cannot be represented as an int
int64(Huge) // 1267650600228229401496703205376 cannot be represented as an int64
Four * 300 // operand 300 cannot be represented as an int8 (type of Four)
Four * 100 // product 400 cannot be represented as an int8 (type of Four)

补码使用的一元操作符 ^ 对于非常量的匹配模式:补码对于无符号常量为 1,对于有符号和无类型常量为 -1。

1
2
3
4
5
^1         // untyped integer constant, equal to -2
uint8(^1) // illegal: same as uint8(-2), -2 cannot be represented as a uint8
^uint8(1) // typed uint8 constant, same as 0xFF ^ uint8(1) = uint8(0xFE)
int8(^1) // same as int8(-2)
^int8(1) // same as -1 ^ int8(1) = -2

实现限制:编译器在处理无类型浮点数和复数时会取近似值;具体请看常量章节。这个取近似值的操作在浮点数在整数上下文时会产生无效值,即使在计算过后是一个整型。

运算优先级

在包级别,初始化的依赖性由变量声明的初始化表达式顺序决定。否则,当计算表达式内的操作数时,赋值,返回语句,所有函数调用,方法调用,和通信操作都会由左向右计算。

例如,在函数作用域中的赋值:

1
y[f()], ok = g(h(), i()+x[j()], <-c), k()

函数调用和通信的发生顺序为:f()h()i()j()<-cg()k()。但是对 y 和 x 的取值操作没有指定。

1
2
3
4
5
a := 1
f := func() int { a++; return a }
x := []int{a, f()} // x may be [1, 2] or [2, 2]: evaluation order between a and f() is not specified
m := map[int]int{a: 1, a: 2} // m may be {2: 1} or {2: 2}: evaluation order between the two map assignments is not specified
n := map[int]int{a: f()} // n may be {2: 3} or {3: 3}: evaluation order between the key and the value is not specified

在包级别,依赖的初始化顺序会覆盖这个从左向右的规则:

1
2
3
4
5
6
7
var a, b, c = f() + v(), g(), sqr(u()) + v()

func f() int { return c }
func g() int { return a }
func sqr(x int) int { return x*x }

// functions u and v are independent of all other variables and functions

语句

语句控制程序的执行。

1
2
3
4
5
6
7
Statement =
Declaration | LabeledStmt | SimpleStmt |
GoStmt | ReturnStmt | BreakStmt | ContinueStmt | GotoStmt |
FallthroughStmt | Block | IfStmt | SwitchStmt | SelectStmt | ForStmt |
DeferStmt .

SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | ShortVarDecl .

终止语句

终止语句会阻止相同代码块中下面所有语句的执行。以下语句属于终止语句:

  1. returngoto 语句

  2. 对内置 panic 函数的调用

  3. 代码块结束

  4. if 语句中:

  5. else 分支

  6. 所有分支末尾

  7. for语句中:

  8. break 语句和循环结束

  9. switch 语句:

  10. switch 语句中没有 break 语句,

  11. 有一个默认的 case

  12. 语句列表中的每个 case 语句和有可能存在的 fallthrough 语句

  13. select 语句中:

  14. 没有 break 语句

  15. 每个 case 中的语句列表,如果包含默认 case

所有其他语句都不是中断语句。

如果语句序列不为空并且最后一个非空语句是终止语句,那么语句序列就以终结语句结尾。

空语句

空语句不做任何事情。

1
EmptyStmt = .

标签语句

标签语句可以作为 gotobreakcontinue 语句的目标。

1
2
LabeledStmt = Label ":" Statement .
Label = identifier .
1
Error: log.Panic("error encountered")

表达式语句

除了特定的内置函数,一般的函数、方法和接收操作都可以出现在表达式语句的上下文中。这些语句可以使用括号括起来。

1
ExpressionStmt = Expression .

下面的内置函数不允许出现在语句的上下文中:

1
2
append cap complex imag len make new real
unsafe.Alignof unsafe.Offsetof unsafe.Sizeof
1
2
3
4
5
h(x+y)
f.Close()
<-ch
(<-ch)
len("foo") // illegal if len is the built-in function

发送语句

发送语句可以向通道发送一个值。通道表达式必须是通道类型,通道方向必须允许发送操作,并且值类型是可以分配给通道元素通道类型。

1
2
SendStmt = Channel "<-" Expression .
Channel = Expression .

通道类型和值表达式会在发送之前求值。发送操作会一致阻塞,直到可以进行发送操作。如果接收者已经准备好向没有缓存的通道发送值可以立即执行。如果通道内还有缓存空间,向通道内发送值也会立即执行。向关闭的通道发送数据会导致运行时恐慌。像值为 nil 的通道发送数据会一直阻塞。

1
ch <- 3  // send value 3 to channel ch

递增/递减语句

“++” 和 “–” 语句可以递增或者递减运算元一个无类型常量 1。作为一个赋值语句,运算元必须是可寻址的或者 map 的索引表达式。

1
IncDecStmt = Expression ( "++" | "--" ) .

下面的赋值语句在语义上是等价的:

1
2
3
IncDec statement    Assignment
x++ x += 1
x-- x -= 1

赋值

1
2
3
Assignment = ExpressionList assign_op ExpressionList .

assign_op = [ add_op | mul_op ] "=" .

所有左侧运算元都必须是可寻址的、map 索引表达式或空标识符其中之一。运算元可以用括号括起来。

1
2
3
4
x = 1
*p = f()
a[i] = 23
(k) = <-ch // same as: k = <-ch

对于赋值操作 x op= y 其中 op 为二元运算符,它和 x=x op (y) 是等价的,不过它只计算一次 x。op= 是单独的一个词汇单元,在赋值操作中左侧表达式和右侧表达式必须都是单值表达式,并且左侧表达式不能是空白标识符。

1
2
a[i] <<= 2
i &^= 1<<n

元祖赋值语句会把运算返回的多个值分别分配给变量列表。它有两种格式,第一种:它是返回多值的表达式,例如函数调用、通道和 map 运算、类型断言。左侧运算元的数量必须等于返回值的数量。如果函数返回两个值:

1
x, y = f()

它会将第一个返回值分配给 x ,把第二个返回值分配给 y。第二种格式中,左侧运算元的数量必须等于右侧运算元的数量。每个表达式都只能返回单一值,右侧第 n 个值会赋值给左侧第 n 个变量。

1
one, two, three = '一', '二', '三'

空标识符可以在分配时忽略一个右面位置的表达式:

1
2
_ = x       // evaluate x but ignore it
x, _ = f() // evaluate f() but ignore second result value

赋值分为两个阶段。首先会计算左侧运算元的索引表达式和指针的解引用工作并以一定顺序计算右侧表达式的值。

然后依次对左侧运算元赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
a, b = b, a  // exchange a and b

x := []int{1, 2, 3}
i := 0
i, x[i] = 1, 2 // set i = 1, x[0] = 2

i = 0
x[i], i = 2, 1 // set x[0] = 2, i = 1

x[0], x[0] = 1, 2 // set x[0] = 1, then x[0] = 2 (so x[0] == 2 at end)

x[1], x[3] = 4, 5 // set x[1] = 4, then panic setting x[3] = 5.

type Point struct { x, y int }
var p *Point
x[2], p.x = 6, 7 // set x[2] = 6, then panic setting p.x = 7

i = 2
x = []int{3, 5, 7}
for i, x[i] = range x { // set i, x[2] = 0, x[0]
break
}
// after this loop, i == 0 and x == []int{3, 5, 3}

在赋值语句中每个值都必须能分配给左侧指定类型的值。除了以下特例:

  1. 任何类型都能分配给空标识符。

  2. 如果把无类型常量分配给接口类型或者空标识符,它会转换成默认类型。

  3. 如果无类型的布尔值分配给了接口类型或者空标识符,它会先转换成 bool 类型。

if 语句

if 语句根据布尔值表达式的值来决定执行条件分支的代码。如果表达式为真,就执行 if 分支内的代码,否则执行 else 分支的代码。

1
IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
1
2
3
if x > max {
x = max
}

表达式可能先于普通语句,它会在表达式求值之前发生。

1
2
3
4
5
6
7
if x := f(); x < y {
return x
} else if x > z {
return z
} else {
return y
}

switch 语句

for 语句

for 语句可以用来重复执行一段代码。它有三种格式:迭代器可以是单一条件、for 分句或者 range 语句。

1
2
ForStmt = "for" [ Condition | ForClause | RangeClause ] Block .
Condition = Expression .
单一条件的 for 语句

这种情况下 for 会在条件为 true 时一直重复。条件会在每次迭代时都重新计算。如果没有指定条件,默认一直为 true。

1
2
3
for a < b {
a *= 2
}
带分句的 for 语句

带分句的 for 语句也是由条件控制,只是它有一个初始化和寄送的过程。例如赋值、递增或者递减语句。初始化语句可以是短变量声明,但是寄送语句不能。在初始化语句中声明的变量可以在迭代过程中使用。

1
2
3
ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .
InitStmt = SimpleStmt .
PostStmt = SimpleStmt .
1
2
3
for i := 0; i < 10; i++ {
f(i)
}

如果初始化语句非空,它会在进入迭代前执行一次;post 语句在每次循环后都会执行一次。在只有条件的情况下可以省略分号。如果缺省条件语句,默认为 true。

1
2
for cond { S() }    is the same as    for ; cond ; { S() }
for { S() } is the same as for true { S() }
带 range 分句的 for 语句

带 range 分句的 for 语句可以访问数组、切片、字符串、map 的所有元素,还可以从通道中接收值。迭代获得元素分配给了相应的迭代变量并执行代码块。

1
RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .

右侧的 range 分句表达式叫做 range 表达式,它可能是数组、数组的指针、切片、字符串、map 或通道接收者类型。在分配时,左侧运算元必须是可寻址的或者 map 的索引表达式;它们作为迭代变量。如果 range 表达式是一个通道类型,至少需要有一个变量,它也可以有两个变量。如果迭代变量是空标识符,就代表在分句中不存在该标识符。

1
2
3
4
5
6
Range expression                          1st value          2nd value

array or slice a [n]E, *[n]E, or []E index i int a[i] E
string s string type index i int see below rune
map m map[K]V key k K m[k] V
channel c chan E, <-chan E element e E
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
var testdata *struct {
a *[7]int
}
for i, _ := range testdata.a {
// testdata.a is never evaluated; len(testdata.a) is constant
// i ranges from 0 to 6
f(i)
}

var a [10]string
for i, s := range a {
// type of i is int
// type of s is string
// s == a[i]
g(i, s)
}

var key string
var val interface {} // element type of m is assignable to val
m := map[string]int{"mon":0, "tue":1, "wed":2, "thu":3, "fri":4, "sat":5, "sun":6}
for key, val = range m {
h(key, val)
}
// key == last map key encountered in iteration
// val == map[key]

var ch chan Work = producer()
for w := range ch {
doWork(w)
}

// empty a channel
for range ch {}

Go 语句

go 语句会开始在相同地址空间中的单独 goroutine 中调用函数。

1
GoStmt = "go" Expression .

表达式必须是函数或者方法调用;它不能使用括号括起来,调用内置函数有表达式语句的限制。

函数的值和参数会按顺序在调用的 goroutine 中求值。不像普通的函数调用,程序不会等待函数调用完成,而是直接开启一个新的 goroutine 执行函数。函数退出时,goroutine 也会退出。函数的任何返回值都会被丢弃。

1
2
go Server()
go func(ch chan<- bool) { for { sleep(10); ch <- true }} (c)

select 语句

select 语句会在接收/发送操作集中选择一个执行。它看起来和 switch 很像,只不过是专门针对通信操作的。

1
2
3
4
5
SelectStmt = "select" "{" { CommClause } "}" .
CommClause = CommCase ":" StatementList .
CommCase = "case" ( SendStmt | RecvStmt ) | "default" .
RecvStmt = [ ExpressionList "=" | IdentifierList ":=" ] RecvExpr .
RecvExpr = Expression .

接收表达式可以将接收表达式的值分配给一个或两个变量。接收表达式必须是一个接收运算元(可以使用括号括起来)。它最多允许有一个 default 语句。

select 语句执行以下几个步骤:

  1. 对于 select 语句的所有分句,接收操作的通道运算元、通道、发送语句的右侧表达式都会执行一次操作。

  2. 如果一个或多个通信同时发生,它会通过一致性随机选择一个执行。如果没有 default 语句,select 语句会一直阻塞。

  3. 除了 default 分句,其他分句只有在开始进行通信的时候才会执行。

  4. 如果 select 分句是一个接收语句,它可以给变量分配值。

  5. 执行 select 分句内的内容。

如果向 nil 通道发送信息在没有 default 分句的情况下会一直阻塞。

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
var a []int
var c, c1, c2, c3, c4 chan int
var i1, i2 int
select {
case i1 = <-c1:
print("received ", i1, " from c1\n")
case c2 <- i2:
print("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
print("received ", i3, " from c3\n")
} else {
print("c3 is closed\n")
}
case a[f()] = <-c4:
// same as:
// case t := <-c4
// a[f()] = t
default:
print("no communication\n")
}

for { // send random sequence of bits to c
select {
case c <- 0: // note: no statement, no fallthrough, no folding of cases
case c <- 1:
}
}

select {} // block forever

return 语句

return 语句会终止函数 F 的执行并可选的返回一个或多个返回值。所有的滞后函数都会在 F 返回到它的调用者之前执行。

1
ReturnStmt = "return" [ ExpressionList ] .

如果函数没有返回值类型,return 不能返回任何值。

1
2
3
func noResult() {
return
}

有三种方式能够返回指定类型的值:

  1. 返回值可以直接在 return 语句中列出。每个表达式都必须返回一个值并且能够分配给相应的返回值类型。
1
2
3
4
5
6
7
func simpleF() int {
return 2
}

func complexF1() (re float64, im float64) {
return -7.0, -4.0
}
  1. return 语句的表达式列表可以是一个返回多值的函数调用。这时会使用临时变量来获取函数调用的返回值并直接将其作为 return 语句的表达式列表。
1
2
3
func complexF2() (re float64, im float64) {
return complexF1()
}
  1. 如果制定了返回值的标识符那么 return 的表达式列表可以为空。返回值参数会作为普通的本地变量按需分配。return 语句会直接返回它们。
1
2
3
4
5
6
7
8
9
10
func complexF3() (re float64, im float64) {
re = 7.0
im = 4.0
return
}

func (devnull) Write(p []byte) (n int, _ error) {
n = len(p)
return
}

不管如何声明,所有的返回值都会在进入函数前提前初始化成类型的零值。return 语句会在所有 defer 函数之前指定返回值。

实现限制:编译器不允许在覆盖了命名返回值的作用域中直接返回。

1
2
3
4
5
6
func f(n int) (res int, err error) {
if _, err := f(n-1); err != nil {
return // invalid return statement: err is shadowed
}
return
}

break 语句

break 语句会在 forswitchselect 语句内部退出到相同函数的某个位置。

1
BreakStmt = "break" [ Label ] .

如果想指定标签,它必须出现在它所中止的 forswitchselect 语句旁。

1
2
3
4
5
6
7
8
9
10
11
12
13
OuterLoop:
for i = 0; i < n; i++ {
for j = 0; j < m; j++ {
switch a[i][j] {
case nil:
state = Error
break OuterLoop
case item:
state = Found
break OuterLoop
}
}
}

continue 语句

continue 语句会提前 for 语句的下一次迭代。for 语句必须和 continue 在相同函数中。

1
2
3
4
5
6
7
8
9
RowLoop:
for y, row := range rows {
for x, data := range row {
if data == endOfRow {
continue RowLoop
}
row[x] = data + bias(x, y)
}
}

goto 语句

goto 会将程序跳转到相同函数的指定标签处。

1
GotoStmt = "goto" Label .
1
goto Error

goto 语句不允许跳过作用域内程序变量的初始化工作。

1
2
3
goto L  // BAD
v := 3
L:

上面的程序是错误的,因为它跳过了变量 v 的初始化过程。

1
2
3
4
5
6
7
8
9
10
if n%2 == 1 {
goto L1
}
for n > 0 {
f()
n--
L1:
f()
n--
}

标签作用域外的 goto 语句不能跳转到标签处,所以上面的代码是错误的。

Fallthrough 语句

fallthrough 语句会跳转到 switch 语句中的下一个 case 分句中。它应该只在最后一个非空分句中使用。

1
FallthroughStmt = "fallthrough" .

Defer 语句

defer 语句会在包裹函数返回后触发函数调用。这里的返回泛指函数因为 return 语句终止、到达函数末尾或者当前 goroutine 触发运行时恐慌。

1
DeferStmt = "defer" Expression .

表达式必须是函数或者方法调用;它不能使用括号括起来,调用内置函数会有一些限制。

每次执行 defer 语句执行时都会计算函数的参数和值,但是并不会调用函数。相反,函数的调用是在包裹函数返回后进行,它们的执行顺序与声明顺序正好相反。如果 defer 对应的函数值为 nil,会在调用函数的时候导致运行时恐慌而不是声明 defer 语句的时候。

例如:当 defer 函数为函数字面值且包裹函数具有命名结果值,此时,我们在defer 函数中可以访问和修改命名的结果值。defer 函数的所有返回值都会被忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
lock(l)
defer unlock(l) // unlocking happens before surrounding function returns

// prints 3 2 1 0 before surrounding function returns
for i := 0; i <= 3; i++ {
defer fmt.Print(i)
}

// f returns 1
func f() (result int) {
defer func() {
result++
}()
return 0
}

内置函数

内置函数是预定义的。调用他们和其他函数一样只是他们接受一个类型而不是一个表达式。

内置函数没有标准的 Go 类型,所以他们只能作为调用表达式;而不能作为函数的值。

Close

对于管道类型 c,内置函数 close(c) 意味着不在有数据插入到管道中。如果 c 是一个只接收数据的管道,会发生错误。向已经关闭的发送数据或者重复关闭已经关闭的管道会导致运行时恐慌。关闭 nil 管道会引起运行时恐慌。调用 close 后所有之前发送的数据都能接收到,并且在最后不会阻塞而返回零值。多值的接收操作能够返回接收到的数据和表示管道是否关闭的布尔值。

长度和容积

内置函数 lencap 可以接收多种类型的参数,并且返回一个 int 类型结果值。函数的实现能够确保结果值不会溢出。

1
2
3
4
5
6
7
8
9
10
11
Call      Argument type    Result

len(s) string type string length in bytes
[n]T, *[n]T array length (== n)
[]T slice length
map[K]T map length (number of defined keys)
chan T number of elements queued in channel buffer

cap(s) [n]T, *[n]T array length (== n)
[]T slice capacity
chan T channel buffer capacity

切片的容积底层数组包含的元素个数。在任何情况下都有以下关系:

1
0 <= len(s) <= cap(s)

nil 切片,map,或者 channel 的长度都为 0。nil 切片,管道的容积都为 0。

表达式 len(x)s 是字符串常量时也为常量。如果 s 为数组或者指向数组的指针并且表达式 s 不包含 channel 接收器或者函数调用那么 len(s)cap(s) 也是常量;在这个情况下 s 时不能求值的。其他情况下 lencap 不是常量并且 s 是可以求值的。

1
2
3
4
5
6
7
8
const (
c1 = imag(2i) // imag(2i) = 2.0 is a constant
c2 = len([10]float64{2}) // [10]float64{2} contains no function calls
c3 = len([10]float64{c1}) // [10]float64{c1} contains no function calls
c4 = len([10]float64{imag(2i)}) // imag(2i) is a constant and no function call is issued
c5 = len([10]float64{imag(z)}) // invalid: imag(z) is a (non-constant) function call
)
var z complex128
内存分配

内置函数 new 接收一个类型 T,它会在运行时给变量分配内存,并且返回一个指向类型 T*T 类型指针。变量的初始化在初始化值章节中介绍。

1
new(T)

例如:

1
2
type S struct { a int; b float64 }
new(S)

给 S 类型的变量分配空间,并初始化它(a=0b=0.0),并且返回一个 *S 类型值保存变量所在的位置。

创建切片,map 和 管道

内置函数 make 以一个类型作为参数,它必须是一个切片,map 或者管道类型,它返回一个 T 类型的值,而不是(*T)类型,它会按初始化值章节描述的方式进行初始化。

1
2
3
4
5
6
7
8
9
10
Call             Type T     Result

make(T, n) slice slice of type T with length n and capacity n
make(T, n, m) slice slice of type T with length n and capacity m

make(T) map map of type T
make(T, n) map map of type T with initial space for approximately n elements

make(T) channel unbuffered channel of type T
make(T, n) channel buffered channel of type T, buffer size n

n 和 m 必须是整数类型或者无类型常量。一个常量参数不能为负数并且该值在 int 类型的范围内;如果它是无类型常量,会被转换成 int 类型。如果 n 和 m 都是常量,那么 n 必须大于 m。如果 n 是负数或者大于 m 会引发运行时 panic。

1
2
3
4
5
6
s := make([]int, 10, 100)       // slice with len(s) == 10, cap(s) == 100
s := make([]int, 1e3) // slice with len(s) == cap(s) == 1000
s := make([]int, 1<<63) // illegal: len(s) is not representable by a value of type int
s := make([]int, 10, 0) // illegal: len(s) > cap(s)
c := make(chan int, 10) // channel with a buffer size of 10
m := make(map[string]int, 100) // map with initial space for approximately 100 elements

使用 make 来指定大小初始化 map 类型将会创建一个预留 n 个元素空间的 map 类型。更详细的行为依赖于具体实现。

追加或者拷贝切片

内置函数 appendcopy 可以进行切片的通用操作。对于这两个函数,一个是拷贝内存,一个是引用内存。

可变参数的函数 append 可以向切片 s 中追加一个或多个 x 值,并返回这个切片。传进 ...T 的值会根据参数传值。作为特例,append 在 s 为 []byte 切片时,可以使用字符串后面跟 ... 作为参数。

如果 s 的容积容纳不下这些元素,那么 append 会分配一个新的足够大的数组。否则会使用原来的底层数组。

1
2
3
4
5
6
7
8
9
10
11
s0 := []int{0, 0}
s1 := append(s0, 2) // append a single element s1 == []int{0, 0, 2}
s2 := append(s1, 3, 5, 7) // append multiple elements s2 == []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...) // append a slice s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...) // append overlapping slice s4 == []int{3, 5, 7, 2, 3, 5, 7, 0, 0}

var t []interface{}
t = append(t, 42, 3.1415, "foo") // t == []interface{}{42, 3.1415, "foo"}

var b []byte
b = append(b, "bar"...) // append string contents b == []byte{'b', 'a', 'r' }

copy 函数从 src 拷贝原属到 dst 并且返回拷贝元素的个数。参数中所有的元素类型必须是 T 类型或者能转换成 T 的类型。拷贝元素的数量是 len(src)len(dst) 中的较小值。作为特例,copy 可以从 string 类型拷贝元素到 []byte 类型。这会把字符串中的元素拷贝到字节切片中。

1
2
copy(dst, src []T) int
copy(dst []byte, src string) int

例:

1
2
3
4
5
6
var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int, 6)
var b = make([]byte, 5)
n1 := copy(s, a[0:]) // n1 == 6, s == []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:]) // n2 == 4, s == []int{2, 3, 4, 5, 4, 5}
n3 := copy(b, "Hello, World!") // n3 == 5, b == []byte("Hello")
删除 map 中的元素

内置函数 delete 移除 map 类型 m 中的键值 k。k 的类型必须是能够转换成 m 键类型的类型。

1
delete(m, k)  // remove element m[k] from map m

如果 map 类型 m 是 nil 或者 m[k] 不存在,那么 delete 函数不做任何事情。

操作复数

有三个函数可以组装或者分解复数。内置函数 complex 会构造一个复数,realimag 会分解出复数的实部和虚部。

1
2
3
complex(realPart, imaginaryPart floatT) complexT
real(complexT) floatT
imag(complexT) floatT

参数的类型和返回值类型是对应的。对于 complex,两个参数必须是相同的浮点类型,并返回由相同浮点数组成的复数类型。complex64float32 对应的类型,complex128float64 对应的参数类型。如果参数是一个无类型常量,它会转换成另一个参数的类型。如果两个参数都是无类型常量,他们必须实数或者虚数部分为零,并且它会返回一个无类型的复数常量。

realimag 函数和 complex 正好相反的,所以对于一个值复数类型 Z 的值 z,z==Z(complex(real(z),imag(z)))

如果这么操作都是常量,那么返回的值也是常量。

1
2
3
4
5
6
7
8
9
10
var a = complex(2, -2)             // complex128
const b = complex(1.0, -1.4) // untyped complex constant 1 - 1.4i
x := float32(math.Cos(math.Pi/2)) // float32
var c64 = complex(5, -x) // complex64
var s uint = complex(1, 0) // untyped complex constant 1 + 0i can be converted to uint
_ = complex(1, 2<<s) // illegal: 2 assumes floating-point type, cannot shift
var rl = real(c64) // float32
var im = imag(a) // float64
const c = imag(b) // untyped constant -1.4
_ = imag(3 << s) // illegal: 3 assumes complex type, cannot shift
处理 panic

两个内置函数 panicrecover,可以抛出和处理运行时 panic 和程序的错误条件。

1
2
func panic(interface{})
func recover() interface{}

当执行 F 函数时,显式的调用 panic或者运行时 panic 都会中断 F 的执行。但是 F 中的延迟函数还会执行。接下来调用 F 函数处的延迟函数也会执行,一直到顶级的延迟函数。鉴于这点,程序关闭并且错误条件可以抛出。包括 panic 中的值。这个顺序叫做 panicking

1
2
3
panic(42)
panic("unreachable")
panic(Error("cannot parse"))

recover 函数允许程序从一个 panicking 中恢复执行。假设函数 G 延迟执行函数 D ,在 D 中调用 recover 这时如果在 G 执行时发生 panic 会在 D 中恢复。当函数执行到 D,recover 的返回值会返回 panic 对应的错误,并且终止 panicking 。在这个情况下 G 函数和 panic 之间的代码不会执行。任何在 D 中 G 之前的延迟函数会返回到调用者。

在下面两种情况下 recover 会返回 nil:

  • panic 的参数为 nil

  • 携程里没有发生 panic

  • recover 不是在延迟函数中执行

本例中的 protect 函数会在 g 发生 panic 的时候恢复执行。

1
2
3
4
5
6
7
8
9
10
func protect(g func()) {
defer func() {
log.Println("done") // Println executes normally even if there is a panic
if x := recover(); x != nil {
log.Printf("run time panic: %v", x)
}
}()
log.Println("start")
g()
}
初始化

这个实现提供了多个内置函数来帮助进行初始化。这些函数用来输出信息但是不确定会一直存在于语言中,他们都没有返回值。

1
2
3
4
Function   Behavior

print prints all arguments; formatting of arguments is implementation-specific
println like print but prints spaces between arguments and a newline at the end

实现限制:printprintln 不接受除了布尔值,数字,字符串以外的其他类型。

程序的初始化和执行

零值

当为变量分配内存空间时,不管是声明还是调用 new 或者使用字面值和 make 初始化,只要创建了一个新值变量都会有一个默认值。这样的元素和值会使用它类型的零值:false 是布尔值的零值,0 为数值类型零值,”” 为字符串零值,nil 为指针,函数,接口,切片,频道,字典。初始化会递归完成,所以结构体里的数组中的元素也都会有它自己的零值。

下面两个声明时相等的:

1
2
var i int
var i int = 0

请看下面的声明:

1
2
3
4
5
type T struct { i int; f float64; next *T }
t := new(T)
t.i == 0
t.f == 0.0
t.next == nil

这和下面的声明时同等效果的:

1
var t T
包的初始化

保级变量会按声明的顺序进行初始化,如果依赖其他变量,则会在其他变量之后进行初始化。

更确切的说,如果包级变量还没初始化并且没有初始化表达式或者表达式中不包含对其他未初始化变量的依赖,那么会认为它正在等待初始化。初始化过程会从最早声明的变量开始向下一个包级变量重复,直到没有需要初始化的变量。

如果在初始化过程完成后还有未初始化的变量,那么这些变量可能是循环初始化了,这事程序不是合法的。

在多个文件中变量的声明顺序会依据编译时文件出现的顺序:声明在第一个文件中的变量优先于第二个文件中声明的变量,依此类推。

对依赖关系的分析不会根据变量的具体值,它只分析在源码中是否引用了其他变量。例如,如果变量 x 的初始化表达式引用了变量 y 那么 x 就依赖于 y:

  • 引用一个变量或者函数中用到了一个变量

  • 引用了一个方法值 m 或者方法表达式 t.m (这里的静态类型 t 不是借口类型,并且方法 mt 方法集中的方法)。t.m 的返回值不会在此时影响。

  • 变量,函数,或者方法 x 依赖变量 y

依赖分析会在每个包中执行;他只考虑当前包中的析变量,函数,和方法。

例如,给定声明:

1
2
3
4
5
6
7
8
9
10
11
var (
a = c + b
b = f()
c = f()
d = 3
)

func f() int {
d++
return d
}

初始化顺序为 d,b,c,a。

变量可以在包中声明的初始化函数 init 中进行初始化,它没有参数和返回值。

1
func init() {}

可以为每个包定义多个该函数,甚至在一个文件中也可以。并且不会声明该该标识符。因此 init 函数不能在程序中调用。

还未导入的包会先初始化包级的变量然后按照 init 函数在源码中的顺序调用,它可能在包的多个文件中。如果需要导入一个包,它会在初始化自己之前先初始化这个需要导入的包。如果导入一个包多次,那这个包只会初始化一次。导入的包不能存在循环引用。

包的初始化——变量初始化和对 init 函数的调用会按顺序发生在同一个 goroutine 中。 init 函数可能会启动其他 goroutine。不过一般 init 函数都是按序进行初始化的:它只在上一步已经执行完成时才会调用下一个步骤。

确保初始化行为是可以复现的,构建系统鼓励在同一个包中包含多个文件这些文件在编译器中会以字母排序。

程序执行

一个完整的程序由一个 main 包导入所有需要的包。main 包必须以 main 作为包名并且声明一个没有参数和返回值的 main 函数。

1
func main() {}

程序先初始化 main 包然后调用 main 函数。当 main 函数返回时,程序就会退出。它不会等待其他 goroutines 完成。

错误

预定义的错误类型为:

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

它是表示错误信息的常规接口,nil 代表没有发生错误。例如,在文件中读取数据可以定义为:

1
func Read(f *File, b []byte) (n int, err error)

运行时恐慌

运行时错误(例如数组的越界访问)会造成运行时恐慌,它和以 runtime.Error 接口实现调用内置的 panic 函数一样。runtime.Error 满足预定义的 error 接口。不同的错误值代表不同的运行时错误条件。

1
2
3
4
5
6
package runtime

type Error interface {
error
// and perhaps other methods
}

系统相关

unsafe 包

unsafe 是编译器已知的内置包,可以通过导入路径 unsafe 访问包内容,提供 unsafe 包目的是支持底层编程(包括操作非 Go 类型的数据结构)。使用 unsafe 包必须自己保证类型安全而且它有可能破坏程序的移植性。unsafe 包提供了以下接口:

1
2
3
4
5
6
7
8
package unsafe

type ArbitraryType int // 任意一个 Go 类型;它不是一个具体的类型。
type Pointer *ArbitraryType

func Alignof(variable ArbitraryType) uintptr
func Offsetof(selector ArbitraryType) uintptr
func Sizeof(variable ArbitraryType) uintptr

Pointer 是一个指针类型,但是不能解引用 Pointer 的值。所有底层类型 uintptr 的指针和值都能转换成 Pointer 类型,反之亦然。Pointeruintptr 之间的转换效果由具体实现定义。

1
2
3
4
5
6
7
var f float64
bits = *(*uint64)(unsafe.Pointer(&f))

type ptr unsafe.Pointer
bits = *(*uint64)(ptr(&f))

var p ptr = nil

假设变量 v 由 var v = x 定义。Alignof 以表达式 x 作为参数并返回 x 的对齐字节数。Sizeof 以表达式 x 作为参数并返回 x 的大小。

函数 Offsetof 以选择器 s.f( s 或者 *s 结构体中的 f 字段)作为参数,返回字段相对结构体首地址的位置。如果 f 是一个嵌入字段,那 f 必须可以直接访问(不能通过指针进行间接访问)。对于结构体 s 的 f 字段:

1
uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f) == uintptr(unsafe.Pointer(&s.f))

计算机的体系结构要求对齐内存地址(对于一个变量的地址有多种因素影响对齐)。Alignof 函数获取一个人和类型的表达式并返回变量对齐的字节数。对于变量 x:

1
uintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0

编译时 uintptr 类型常量表达式会调用 AlignofOffsetof,和 Sizeof

确定的大小和对齐字节数

对于数字类型,确定有以下尺寸:

1
2
3
4
5
6
7
type                                 size in bytes

byte, uint8, int8 1
uint16, int16 2
uint32, int32, float32 4
uint64, int64, float64, complex64 8
complex128 16

Go 中规定的最小对齐特性:

  1. 对于任意变量类型 x:unsafe.Alignof(x) 至少为 1。

  2. 对于结构体类型:unsafe.Alignof(x) 是所有内部字段 unsafe.Alignof(x.f) 的最大值,并且至少为 1。

  3. 对于数组类型:unsafe.Alignof(x) 和数组元素类型的 alignment 相同。

结构体(数组)在内部没有字段(元素)的时候大小为 0。两个所占空间大小为 0 的不同变量可能在内存中拥有相同地址。

深入 OGNL 与 Mybatis 源代码分析一次 Mybatis 升级引发的线上事故

     项目中对 Mybatis 做了一次升级。前后版本如下,3.2.5 -> 3.4.4:

mybatis前后版本升级

     结果第二天巡检发现如下报错,过了两个小时业务高峰期,前台业务人员不断反馈某最核心的业务无法进行:

报错信息

     我们当时定位到错误的地方,根据经验修改之后验证通过,重新上线之后得以解决。可能涉及敏感数据,所以不展示实际的报错与弥补方案。

     以下是我在本地的问题复现。在本地的一个标准的 SSM 工程中分别引入以下两个版本的 Mybatis 依赖:

分别引入两个版本的依赖

     编写如下数据库脚本:

数据库脚本

     dao 层调用方法如下:

dao层方法调用

     当 Mybatis 依赖为 3.2.5 的较低版本时,不会报错;当 Mybatis 依赖版本为 3.4.4 的较高版本时,则会报出上面的错误:

1
invalid comparision:  java.util.HashMap and java.lang.String

     在本地问题得到复现。问题的关键在于数据库脚本中的 if 条件编译语句的这一个子句 _parameter!=’’ 将_parameter 与 ‘’ 做比较,_parameter 是 Mybatis 的一个内置对象,你不需要知道它的作用,只需要知道他是 Map 类型的就行了,显然 ‘’ 是 String 类型的。到这里我们其实已经猜出来了,正是因为这种不规范的比较导致数据库脚本执行失败(实际上是 Mybatis 编译 SQL 失败)。

     但是问题又来了,为什么 Mybatis 较低版本的时候没有问题,而较高版本则暴露出这个问题了? 我们深入源码分析一下。因为我对 Mybatis 源码比较熟悉,加上实际生产中报错的堆栈信息也很全,所以直接定位到了 Mybatis 的这个类型:

ifnode

     上述代码的作用:在我们上述 SQL 脚本中,根据 if 子句的测试语句(就是 … && _parameter!=’’ 那一坨)判断,当前 if 子句所包裹的 sql 是否需要动态编译进最终的执行sql中。当我们进一步追踪,就进入到了 OGNL 的源码中,OGNL 是一套表达式解析引擎,一直定位下去就到了具体报错的方法。到这里我们补充一下版本依赖关系:

1
2
mybatis-3.2.5  ->  ognl-2.6.9
mybatis-3.4.4 -> ognl-3.1.14

     高版本 OGNL 源码如下:

高版本 OGNL 代码

     低版本 OGNL 源码如下:

低版本 OGNL 代码

     类型标识相关的源码如下:

类型标记

     case 为 NONUMBERIC 的含义是当比较的值是非数值类型,所以 _parameter!=’’ 子句的判断自然是走该分支语句的代码。t1、t2,v1、v2 的含义是两个待比值( _parameter 和 ‘’)的类型和 value,在这个场景中分别是如下调试面板所示的(不明白的请观察为了复现问题所编写的 SQL 脚本和 dao 层语句):

调试信息如下

     解释一下:t1 = t2 = 10,表示 _parameter 与 ‘’ 都是非数值类型。v1 表明了 _parameter 是个 HashMap 类型的变量,有一个 (blurname,cat) 的键值对,v2 = ‘’。另外,类的 Class 实例中有一个 isAssignableFrom 方法,这个方法是用来判断两个类的之间的关联关系,也可以说是一个类是否可以被强制转换为另外一个实例对象。

     至此所需信息全部已经准备完毕,我们可以来分析高低版本 OGNL 的源码了。高版本 OGNL 中,我们直接看 case:NONUMBERIC 的分支子句。代码含义为:

     如果 V1 是 Comparable 类型的并且 V1 可以强转为 V2 的类型,则进入 if 分支,否则进入 else 分支,而 else 分支直接报错,而且报错信息是我们实际生产环境中遇到的。显然,V1 既不是 Comparable 类型,也无法转换为 V2 的类型(HashMap -> String),所以进入了 else 分支,mybatis 升级之后携带 OGNL 的升级,数据库不规范的写法导致 mybatis 编译 sql 语句报错,阻塞了业务

     低版本的 OGNL 的 case:NONUMBERIC 的分支子句的代码逻辑说实话非常拧巴,含义是:

     如果 v1、v2 任一变量为 null,则进入 if 分支,显然不会进入。else 先判断v1、v2 是否能互转,显然不能,直接跳过。接下来是重中之重:如果 equals 为 true ,跳出 case,否则报错。我们根据结果看,equals 必定为 true,因为我们那种不规范的 mybatis 在这个地方,它每没报错——事实上是应该将该问题抛出来的,从而引导开发者更正 mybatis 脚本。接下来我们看方法外面这个 equals 的来源:

equals

     我惊呆了,直接写死传经来的,至于这个 equals 意欲何为,当初作者为什么这么写,也许只有作者自己知道。反正高版本的 OGNL 已经将这部分的代码逻辑全部重构了。

     我们可以得到如下结论: _低版本的 mybatis 依赖了低版本的 OGNL ,低版本的 OGNL 在上述分析的函数中存在一定缺陷,这个缺陷会导致我们在编写 Mybatis 脚本的时候类似于 parameter!=’’ 的不规范写法不被发现。当我们升级了 Mybatis 之后,这种不规范的写法反而兜不住暴露出来了,加上组件升级测试不充分,直接上到了生产环境。

     反思:

  • 日常开发要严格要求自己,追求正规、大气的编程素养,每一行代码,每一个字符,都要过大脑,不要太随便,不要随便复制粘贴能跑就行。
  • 组件升级要慎之又慎,测试要充分。

GC——全流程

1、minorGC 和 Full GC 区别

     新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。

     老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。

2、minorGC 过程详解

     在初始阶段,新创建的对象被分配到 Eden 区,Survivor 的两块空间都为空。

图一

     当Eden区满了的时候,minor garbage 被触发。

图二

     经过扫描与标记,存活的对象被复制到S0,不存活的对象被回收, 并且存活的对象年龄都增大一岁。

图三

     在下一次的 Minor GC 中,Eden 区的情况和上面一致,没有引用的对象被回收,存活的对象被复制到 Survivor区。当 Eden 和 s0区空间满了,S0 的所有的数据都被复制到S1,需要注意的是,在上次 Minor GC 过程中移动到S0 中的两个对象在复制到 S1 后其年龄要加1。此时 Eden 区 S0 区被清空,所有存活的数据都复制到了 S1 区,并且 S1 区存在着年龄不一样的对象,过程如下图所示:

图四

     再下一次 Minor GC 则重复这个过程,这一次 Survivor 的两个区对换,存活的对象被复制到 S0,存活的对象年龄加1,Eden 区和另一个 Survivor 区被清空。

图五

     再经过几次 Minor GC 之后,当存活对象的年龄达到一个阈值之后(-XX:MaxTenuringThreshold 默认是15),就会被从年轻代 Promotion 到老年代。

图六

     随着 MinorGC 一次又一次的进行,不断会有新的对象被 Promote 到老年代。

图七

     上面基本上覆盖了整个年轻代所有的回收过程。最终,MajorGC将会在老年代发生,老年代的空间将会被清除和压缩(标记-清除或者标记-整理)。从上面的过程可以看出,Eden 区是连续的空间,且 Survivor 总有一个为空。经过一次 GC 和复制,一个 Survivor 中保存着当前还活着的对象,而 Eden 区和另一个 Survivor 区的内容都不再需要了,可以直接清空,到下一次 GC 时,两个 Survivor 的角色再互换。因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将 Eden 区和一个 Survivor 中仍然存活的对象拷贝到另一个 Survivor 中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下(基于大部分对象存活周期很短的事实)高效,如果在老年代采用停止复制,则是非常不合适的。

     老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-压缩算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。在发生 Minor GC 时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次 Full GC,否则,就查看是否设置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行 MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC( 这代表着如果设置-
XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。

3、整体描述

     大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s1(“To”),并且对象的年龄还会加 1( Eden 区 -> Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候,From 和 To 会交换他们的角色,也就是新的 To 就是上次 GC 前的 From ,新的 From 就是上次 GC 前的 To。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到 To 区被填满,To 区被填满之后,会将所有对象移动到年老代中。

4、GC 触发条件

     Minor GC 触发条件:Eden 区满时。Full GC 触发条件:

  • 调用 System.gc 时,系统建议执行 Full GC,但是不必然执行;
  • 老年代空间不足;
  • 方法去空间不足;
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
  • 由 Eden 区、From Space 区向 To Space 区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

5、对象进入老年代的四种情况

     假如进行Minor GC时发现,存活的对象在ToSpace区中存不下,那么把存活的对象存入老年代。

图八

     大对象直接进入老年代:假设新创建的对象很大,比如为5M(这个值可以通过PretenureSizeThreshold这个参数进行设置,默认3M),那么即使Eden区有足够的空间来存放,也不会存放在Eden区,而是直接存入老年代。

图九

     长期存活的对象将进入老年代:此外,如果对象在Eden出生并且经过1次Minor GC后仍然存活,并且能被To区容纳,那么将被移动到To区,并且把对象的年龄设置为1,对象没”熬过”一次Minor GC(没有被回收,也没有因为To区没有空间而被移动到老年代中),年龄就增加一岁,当它的年龄增加到一定程度(默认15岁,配置参数-XX:MaxTenuringThreshold),就会被晋升到老年代中。

     动态对象年龄判定:还有一种情况,如果在From空间中,相同年龄所有对象的大小总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象就会被移动到老年代,而不用等到15岁(默认)。

图十

6、空间分配担保

     在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全的。如果不成立,则虚拟机会查看 HandlerPromotionFailure 这个参数设置的值( true 或 flase )是否允许担保失败(如果这个值为 true,代表着 JVM 说,我允许在这种条件下尝试执行 Minor GC,出了事我负责)。

     如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlerPromotionFailure 为 false,那么这次 Minor GC 将升级为 Full GC。如果老年代最大可用的连续空间大于历次晋升到老年代对象的平均大小,那么 HandlerPromotionFailure 为 true 的情况下,可以尝试进行一次 Minor GC,但这是有风险的,如果本次将要晋升到老年代的对象很多,那么 Minor GC 还是无法执行,此时还得改为 Full GC。

     注意:JDK 6Update 24 之后,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大 小就会进行 Minor GC,否则进行 Full GC。

IP数据报

     IP 协议控制传输的协议单元称为 IP 数据报(IP Datagram,IP数据报、IP包或IP分组)。IP协议屏蔽了下层各种物理子网的差异,能够向上层提供统一格式的IP数据报。lP数据报采用数据报分组传输的方式,提供的服务是无连接方式。IP数据报的格式能够说明lP协议具有什么功能。IPv4数据报由报头和数据两部分组成,其中,数据是高层需要传输的数据,报头是为了正确传输高层数据而增加的控制信息。报头的前一部分长度固定,共20字节,是所有IP数据报必须具有。在首部固定部分的后面是可选字段,长度可变。

IP报文结构

1、固定部分

  • 版本:占 4 位,指 IP 协议的版本。通信双方使用的 IP 协议版本必须一致。广泛使用的 IP 协议版本号为 4(即 IPv4)。关于 IPv6,还处于草案阶段。
  • 首部长度:占 4 位,可表示的最大十进制数值是 15。请注意,这个字段所表示数的单位是 32 位字长(1 个 32 位字长是 4 字节),因此,当 IP 的首部长度为 1111 时(即十进制的 15),首部长度就达到 60 字节。当 IP 分组的首部长度不是 4 字节的整数倍时,必须利用最后的填充字段加以填充。因此数据部分永远在 4 字节的整数倍开始,这样在实现 IP 协议时较为方便。首部长度限制为 60 字节的缺点是有时可能不够用。但这样做是希望用户尽量减少开销。最常用的首部长度就是 20 字节(即首部长度为 0101),这时不使用任何选项。
  • 区分服务:占 8 位,用来获得更好的服务。这个字段在旧标准中叫做服务类型,但实际上一直没有被使用过。1998 年 IETF 把这个字段改名为区分服务DS(Differentiated Services)。只有在使用区分服务时,这个字段才起作用。
  • 总长度:总长度指首部和数据之和的长度,单位为字节。总长度字长为 16 位,因此数据报的最大长度为 2^16-1=65535 字节。在 IP 层下面的每一种数据链路层都有自己的帧格式,其中包括帧格式中的数据字段的最大长度,这称为最大传送单元 MTU(Maximum Transfer Unit)。当一个数据报封装成链路层的帧时,此数据报的总长度(即首部加上数据部分)一定不能超过下面的数据链路层的 MTU 值。
  • 标识:占 16 位。IP 软件在存储器中维持一个计数器,每产生一个数据报,计数器就加1,并将此值赋给标识字段。但这个“标识”并不是序号,因为IP是无连接服务,数据报不存在按序接收的问题。当数据报由于长度超过网络的 MTU 而必须分片时,这个标识字段的值就被复制到所有的数据报的标识字段中。相同的标识字段的值使分片后的各数据报片最后能正确地重装成为原来的数据报。
  • 标志:占 3 位,但只有 2 位有意义。标志字段中的最低位记为 MF(More Fragment)。MF=1 即表示后面“还有分片”的数据报。MF=0 表示这已是若干数据报片中的最后一个。标志字段中间的一位记为 DF(Don’t Fragment),意思是“不能分片”。只有当 DF=0 时才允许分片。
  • 片偏移:占 13 位。片偏移指出:较长的分组在分片后,某片在原分组中的相对位置。也就是说,相对用户数据字段的起点,该片从何处开始。片偏移以 8  个字节为偏移单位。这就是说,除了最后一个分片,每个分片的长度一定是 8 字节(64 位)的整数倍。
  • 生存时间:占 8 位,生存时间字段常用的的英文缩写是 TTL(Time To Live),表明是数据报在网络中的寿命。由发出数据报的源点设置这个字段。其目的是防止无法交付的数据报无限制地在因特网中兜圈子,因而白白消耗网络资源。最初的设计是以秒作为 TTL 的单位。每经过一个路由器时,就把 TTL 减去数据报在路由器消耗掉的一段时间。若数据报在路由器消耗的时间小于 1 秒,就把 TTL 值减 1。当 TTL 值为 0 时,就丢弃这个数据报。后来把 TTL 字段的功能改为“跳数限制”(但名称不变)。路由器在转发数据报之前就把 TTL 值减 1。若 TTL 值减少到零,就丢弃这个数据报,不再转发。因此,TTL 的单位不再是秒,而是跳数。TTL 的意义是指明数据报在网络中至多可经过多少个路由器。显然,数据报在网络上经过的路由器的最大数值是 255。若把 TTL 的初始值设为 1,就表示这个数据报只能在本局域网中传送。
  • 协议:占 8 位,协议字段指出此数据报携带的数据是使用何种协议,以便使目的主机的 IP 层知道应将数据部分上交给哪个处理过程。6 指 TCP 协议,17 指的是 UDP 协议。
  • 首部校验和:占16位。这个字段只检验数据报的首部,但不包括数据部分。这是因为数据报每经过一个路由器,路由器都要重新计算一下首部检验和(一些字段,如生存时间、标志、片偏移等都可能发生变化)。不检验数据部分可减少计算的工作量。
  • 源地址和目的地址:32位,IPV4 地址。

2、可变部分

     IP 首部的可变部分就是一个可选字段。选项字段用来支持排错、测量以及安全等措施,内容很丰富。此字段的长度可变,从 1 个字节到 40 个字节不等,取决于所选择的项目。某些选项项目只需要 1 个字节,它只包括 1 个字节的选项代码。但还有些选项需要多个字节,这些选项一个个拼接起来,中间不需要有分隔符,最后用全 0 的填充字段补齐成为 4 字节的整数倍。增加首部的可变部分是为了增加 IP 数据报的功能,但这同时也使得 IP 数据报的首部长度成为可变的。这就增加了每一个路由器处理数据报的开销。实际上这些选项很少被使用。新的 IP 版本 IPv6 就将 IP 数据报的首部长度做成固定的。这些任选项定义如下:

  • 安全和处理限制(用于军事领域)。
  • 记录路径(让每个路由器都记下它的IP地址)。
  • 时间戳(Time Stamp)(让每个路由器都记下IP数据报经过每一个路由器的IP地址和当地时间)。
  • 宽松的源站路由(Loose Source Route)(为数据报指定一系列必须经过的IP地址)。
  • 严格的源站路由(Strict Source Route)(与宽松的源站路由类似,但是要求只能经过指定的这些地址,不能经过其他的地址)。

     这些选项很少被使用,并非所有主机和路由器都支持这些选项。

  • Copyrights © 2023-2024 杨海波
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信