七叶笔记 » golang编程 » Go语言魅力初体验

Go语言魅力初体验

Go语言简介

Golang是 Google 在2009年开源的 编程语言 。我们思考一个问题,Google为什么会在2009年开源Golang呢?其实内部有几个动意,Google于2009年之前在 Python 上面投入了非常大的精力。Python这门语言由于它的语法简洁,所以使用范围非常广泛。但是,这样就会涉及到一个问题。一门编程语言简单它是一件非常厉害的事情。

而Golang设计者Rob Pike说要把Golang这门编程语言的哲学设计极致。它的特征是没有什么内容可以再删除了,而不是没有什么内容可以再添加进去了。如果一门编程语言入门非常困难,从人群上定义,也许只有10%的人能学会。如果把它设计的在简单一些,可能会有20%的人学会,如果在经过几次优化,把它设计的更加的简单,可能90%的人都可以学会。

Python语言本身就比较简单,所以导致了Python这门编程语言性能不是最强的,也不需要编译等种种原因。导致现在运维在用它、测试在用它、大数据在用它、包括现在非常热门的人工智行业能也在用它。甚至幼儿编程、小学编程、学龄前儿童编程和一些小孩儿玩的机器人都是拿Python写出来的。

简单的东西并不是可以简单做出来的,但是让它做出来就会非常的厉害。我当时学Golang这门语言的时候就觉得它非常的厉害,因为它也是非常简单的。所以说,当时Google在没有Golang这门编程语言之前,他们选择是用Python。但是Python有两个硬伤:首先它运行速度是非常慢的,比如说C语言运行需要1秒钟时间,而Python平均是需要60秒。并不是说Python实现的不好,因为Python是 解释型语言 ,所以跑的比较慢。当然,其它的解释型语言也不比Python语言强到哪里去。包括我们业界统称的3P语言(Python、PHP、Perl)都处于这个水平线,以及Ruby语言也是如此。

当然,Google也是付出了非常大的时间和精力,想从Python解释器的实现层面去想办法。当时他们想把Python这门语言做的非常高效,甚至Google把Python语言的发明人GuidovanRossum都招到了Google公司。这样的一群大牛搞了很久,最后发现这件事貌似是非常困难的。因为它历史包袱太重了,有很多库已经基于Python这么做了,改动任何一点内容都会导致兼容性出现崩溃的情况。包括里面一些代码的风格、方式都和高性能设计不相符。所以说,优化起来也是非常困难的。

Python语言它是解释型语言。解释型语言是什么意思呢?运行一个Python脚本,使用ps命令看该脚本时,你会发现.py文件并不是用ps aux命令可以查看到的,它实际运行的是Python。但是用编译 型语言 编译出来是什么样,它就是什么样。比如 nginx 这种编译型的程序,它就是一个Nginx程序。用Golang编译出来程序,他就是用Golang运行的。

Python的一个简单原理是:当Python读取.py文件时会去理解它每一步想做什么,从头到尾,一步一步往下执行,最后展开执行,这是解释型语言。直接跑到语言里面,汇编指令集,直接cpo,mov多少个计算器,load某个东西到内存里面,最后开始运行,Python是这样的解释。

但是,Python并不是因为解释而带来这么大的overhead,它最大的overhead(天花板)是在于Python是一个解释型语言。所以它里面所有的东西都是object(对象)。为什么它又是一个object呢?其实它也可以不是一个object。它是object并不是因为它是一个解释型语言,解释型语言它不一定是object,它是object一个强原因是Python是一个弱类型语言。因为在Python里面可以写a = 1,代码如下所示。

>>> a = 1
 

一个Python的脚本只写a = 1这一行代码也是可以的。意思也表达的非常明确,我把1赋值给了a。而且Python还支持在后面直接运行a = “b”,代码如下所示。

>>> a = 1
>>> a = "b"
 

从上面代码中可以看出1和b显然是不一样的。最起码1是一个int类型,b是一个 字符串 ,Python它为什么允许把一个变量首先都不用声明直接赋值给1呢?因为Python是一个弱类型语言。它在变量里面没有强制要求必须是个什么样的类型,就像编译型语言C语言和Go语言里面的指针一样的东西,你爱指哪指哪,或者可以解释为是一种动态的内容。这就会导致一个问题,a爱指哪指哪。所以说它只能在内存里面,它是一个object。如果直接在a里面存一个1或者在内存里面存一个int大小的一个1,或者又有人把它赋值成字符串,这岂不是抓瞎了吗。就是因为Python是一个弱类型语言,也是因为这个原因导致的很难优化。

什么是弱类型语言呢?弱类型语言比较通俗易懂,假设你找一个从来没有学过脚本语言的人去学Python,你告诉他a = 1,a = “b”,他并不觉得有什么不妥,除非懂计算机的人知道它一会是个int,一会是个string觉得很奇怪,所以,它的门槛会大大降低。不然的话,a = “b”,他会告诉你is not a string type,Please……。结果刚入门的人看到这个报错后就会不学了,存在的是这样的一个问题。

为什么是object就跑的慢呢?仔细想想,我们现在写了一个a = 1,b = 2,c = a + b,代码如下所示。

>>> a = 1
>>> b = 2
>>> c = a + b
>>> print (c)
3
 

按照上面代码所示写这样一段代码,如果是解释型弱类型语言它是什么样的过程呢?它首先会创建一个Object,可能会是一块内存,Object里面可能会存入很多的信息,可能会存一个type的东西,可能会保存type是一个int,后面可能是buffer size。因为它有可能存的是字符串,所以我们需要知道指定的buffer size是多大。然后会有一个指针p指向1的一个内存。p = & 1(P指向等于1的一个内存的地方),它会是这样一个东西。然后相应的b = 2,它就会有一个内存指向p指向2,最后做相加的时候就会变得很复杂,首先要找到Object a,在Object a里面找到p这个指针指向的一个内存,然后从p指针指向的内存里面找到1,找到1之后,先放到一个地方,然后再去找b,同样方法找到b的2,然后把a和b弄起来。然后发现c又是一个Object,然后在new一个object出来,然后分配type,分配buffer size等各种各样的东西,因为里面可能会涉及到gc和引用等各种各样的东西,然后有了这些东西之后,我在把1+2的这个值3去set到一个内存里面,在拿c的这个指向数据的指针再去指向3的内存。好,以上是关于c = a + b的整个执行过程。

虽然说了这么多,但是CPU也是这么执行的。可能说这只是几纳秒的一个操作,但是想想,如果是一个编译型的强类型语言,会是一个什么状态?它是不是会直接翻译成甚至优化过来,它直接发行a是一个int,b是一个2,它是一个确定了的值了。那么a + b我不说编译型优化,可能编译型优化会直接优化成c = 3。就算不优化它也是直接找到内存,他的变量名可能在内存中都不存在了,直接找到放了a这个内存的变量,可能是在全局变量里面。然后加上b的变量,大概就是两个寻址。然后在一个CPU里面,先Load到CPU计算器,在Load到计算器二中,在把+放到那一边的内存中,整个过程执行完毕。一般来说会比弱类型的解释型语言要快两个数量级左右。因为它要做的事情非常非常少,它不需要分配很多很多的内存,所以说差距也是非常非常大的。各位读者是否明白他为什么慢?

那么,各位读者有没有疑问,强类型语言或者说编译型语言可以这样做,Python只能那样做,那么Python有没有什么好的方法呢?

Python是一个解释型弱类型语言,Python在这些方面也会进行优化,它会想办法去优化自己的运行速度。比如说,Python在运行的时候你会发现在目录里面会产生一些.pyc文件,.pyc文件是优化的一种方式,但是.pyc文件并没有改变它本身这种解释型弱类型语言的方式,pyc仅仅是把.py文件做了一个预处理。意思是说,首先把注释去掉,把长变量名替换成pyc内部里面可以引用的短变量名,做一些其他的东西去优化,相当于是先预处理下py文件,这是一个简单的优化过程。Python小数字里面的优化也非常多,但是这些小数字优化措施仅可能让他少那么一两倍,但是并不能改变和编译型语言两个数量级的差异。

如果你是Python的设计者,我能不能借用一下强类型语言这种概念去优化Python呢?我不想去弄这么多的Object去做一件事情。实际上,如果我这个解释器稍微有点思维,其实Python解释器也是这么做的。首先会把语法解释成语法数,它会推演。现在c = a + b,OK,我发现a和b,那么c是怎么得到的?是用a+b得到的。a可能是怎么算出来的,一步步推演出来的,如果这个解释很容易推演出来,c其实就是1+2的结果。这个解释器可不可以智能一点?我发现了1+2, 其实Open Object和销毁Object这一件事是完全没有必要的,如果稍微做一点优化,它其实可以发现C可以通过两块内存的东西load到内存中,就像编译型内存一样,load进去后,加起来放进去就可以了。如果C里面还有后续的东西,给C分配一个Obeject存起来就可以了。这是一个非常不错的思路,那么有没有人做这样的事情呢?当然是有的,那就是JIT。

好,前面说了很多关于Python还有强弱型语言的介绍,那么我们继续来说一下Go语言的介绍。后面Google优化Python优化不动了,所以他们就自己想自己开发一门语言,于是就拿Google的前两个字母就当这门语言的一个名字,开始设计这门语言,设计这门语言刚开始的时候是两个人,分别是Rob Pike和Ken Thompson。

Rob Pike是自加拿大的程序员,曾经加入贝尔实验室,为 UNIX小组的成员。曾经参与过贝尔实验室九号计划、Inferno,与编程语言 Limbo的开发。他与肯·汤普逊共同开发了UTF-8。

Ken Thompson是美国计算机科学学者和工程师。黑客文化圈子通常称他为”ken”。在贝尔实验室工作期间,Ken Thompson设计和实现了Unix操作系统。他创造了B语言(C语言的前身),而且他是Plan 9操作系统的创造者和开发者之一。2006年,Ken Thompson进入Google公司工作,与他人共同设计了Go语言。他与 丹尼斯·里奇 同为1983年图灵奖得主。 此外,Ken Thompson还参与过正则表达式和UTF-8编码的设计,改进了文本编辑器QED,创造了Ed编辑器。他曾制造过专门用于下国际象棋的电脑”Belle”,并创建了残局数据库。

关于Go语言设计者的照片可以观看图1。

图1

Go语言特性

1.静态编译,它是一个编译型语言,和C/C++类似。Go的编译器去读源代码,把整个语法进行一遍解析,做一定的优化,优化完把它翻译成当前目标机器的机器码。当然,也可以做一些交叉编译,比如在Mac系统上可以编 Linux ,不同的CPU版本都是可以再一台机器上完成的。而且Go这门语言的厉害之处在于它是静态编译的,比如说我们打开一台Linux的服务器。ldd命令可以查看文件或程序所依赖的动态库,当然这仅限于编译型东西才有。

比如我们简单看下ls这个命令,ls命令指向的是/ bin /ls,使用ldd查看/bin/ls所依赖的库有哪些,如图2所示。

图2

如何查看一个文件或者程序是不是编译型呢?我们通过file命令可以查看,比如查看/bin/ls这个程序,如图3所示。如果是ELF就表示是编译型语言。如果有动态库就是链出来的。

图3

动态库主要是用来优化性能和优化内存,比如说很多程序都用到了这些libc、libacl,我就只用在系统中占一份内存,这是一个古老的考虑。但是这也带来了一个问题,我编译一个程序它依赖的动态库如果它在机器上的版本不对,或者不存在它就运行不了,后面会给它的部署或者其它的一些事情带来很大的问题。做过运维的人想必都清楚这件事情,它叫dependence hell(依赖地狱)。因为这些动态库非常恶心,比如A动态库又依赖于B动态库,它简直就像我们捅了马蜂窝一样。本来只想喝点蜂蜜,没想到捅到一堆马蜂蜇了一脸包,典型的赔了夫人又折兵。Linux很多发行版解决最大的问题就是包的依赖问题,为什么有包的依赖问题就是因为这些问题。

Go语言的一个奇特之处、创新之处在于有人也认为它是历史的倒退,它默认是静态编译。如果你看过Go语言编出来的任何程序你会发现它的依赖几乎为零。几乎为零是什么意思呢?我们用Go语言写的Hello World代码来举例。在Linux服务器上编译Hello World这段代码,先创建一个Hello.go文件。在Hello.go文件中粘贴如下所示代码。

package main
 import  "fmt"
func main() {
 fmt.Println("hello golang")
}
 

go build编译hello.go文件,命令如下所示。

[root@golang ~]# go build hello.go 
[root@golang ~]# ll
total 1880
-rw-------. 1 root root 1319 Jul 4 17:26 anaconda-ks.cfg
-rwxr-xr-x 1 root root 1902849 Nov 18 22:44 hello
-rw-r--r-- 1 root root 71 Nov 18 22:43 hello.go
-rw-r--r-- 1 root root 500 Nov 10 22:01 hello.php
drwxr-xr-x 21 root root 4096 Nov 7 16:09 Linux
drwxr-xr-x 3 root root 4096 Nov 17 19:09 www
 

运行.hello可执行程序,命令如下所示。

[root@golang ~]# ./hello 
hello golang
 

使用file命令查看文件的类型也是二进制的ELF类型,命令如下所示。

[root@golang ~]# file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
 

使用ldd 查看./hello可执行程序我们看到的结果会很失望,因为它并没有依赖任何一个动态链接库,显示not a dynamic executable,如下所示。

[root@golang ~]# ldd ./hello
not a dynamic executable
 

那么疑问来了,它不依赖任何动态链接就可以运行吗?此处还要在给各位读者介绍一些内容,不然还是无法理解其中的奥妙和含义。

为什么程序里面有动态链接库和非动态链接库,非动态链接库的代码难道就可以随处运行了吗?比如说把Linux这个版本拷贝到Mac上或者拷贝到Windows上能否运行呢?它到底依赖什么东西呢?非动态链接库的版本,非动态链接executable它只依赖内核的API,它不依赖于lib这种API。如果是一个普通的程序C/C++编译的它一般默认会依赖于动态的链接库。它可能会调用内核的API也可能会调用libc的,它只要调用就会有依赖,但是这种非动态的这种程序,它的依赖只有内核,只要内核的API满足它的要求他就能运行,而且还有一个好消息,由于Linus Benedict Torvalds同学不懈的坚持,就导致内核从2.X版本到现在3.X和4.X版本内核的接口都是兼容的。

比如我编译了一个面向2.6内核的一个版本的executable,我可以放到4.X版本的内核中继续跑,它的内核就保证了这么一件事,我在2.6版本提供接口在4.X版本里面继续提供,如果有修改我不修改,我增加新的。所以说,Go语言写出来的二进制的这种依赖性是非常非常干净的。CentOS7版本可以拿过来直接使用,这是它的一个厉害之处。而CentOS7之前的系统,尤其是CentOS6的操作系统可能会装一些其他的程序来达到正常使用的效果。

为什么在静态编译这里说了这么多内容,我感觉这是Go语言之所以现在大行其道的关键性原因之一。因为大家长久以来要么被依赖恶心,要么被脚本低下的执行效率恶心,就算是Python它也存在很多的依赖,写过Python脚本的人都知道。当然, Java 也存在很多的依赖库。

2.垃圾回收,这个现在也是见怪不怪了,基本上现在所有的编程语言,除了C(C++有一些智能指针的东西可以做垃圾回收)语言以外都内置了自动垃圾回收。因为内存这件事对于很多程序员来说对于心智负担太重了,很容易泄露,但是我觉得还好吧。并没有觉得是一个很大的负担。而且带垃圾回收的语言在一个行业上是无法使用,比如高频交易、量化投资这些行业它不会用垃圾回收语言,因为它会有gc和停顿。

3.简洁的符号和语法:Go语言的语法现在来讲算是语法简洁性第一阵营里的,它的语法结构不比Python多,也比PHP和Ruby少很多。

4.平坦的类型系统:Go语言中的类型系统还算是比较平坦的,它没有太多的很复杂的类型,它的类型控制的很好,基本上最高层面类型大概在在10个以内就可以包含所有的类型。

5.基于CSP的并发模型:

6.高效简单的工具链:Go语言做出来时他们很在意一件事情:编译速度。项目用Go去build需要多长时间。这个在很多没有接触过超级大项目的人来说觉得我编译还需要编译很久吗?但是在一些大的系统,比如像 微软 和Google这种公司,编译一下他们大的产品的代码库编译可能需要一天,并不是说机器不行,机器都是顶级的(SSD硕大无比,CPU和内存都很大)。但是还是要编一天。这也是一个老生常谈的问题。C/C++编有的时候太慢,Go语言在编译上要比C/C++快很多。这也是因为Google内生的一个需求。

7.丰富的标准库:它说的是丰富的标准库而不是丰富的库。如果拿丰富的库来说,Go现在有非常多的库,语言好用了、火了后库自然而然就很多了。丰富的标准库是说很多东西你不需要去引用 github 上的任何东西都是可以在内部做到的。但是Go语言和PHP、Python、Ruby、Perl语言比起来,它的库也没比前面提到的几个语言库丰富太多。基本上是一个水平,基本上常见的需求都不需要去在外部找一些东西去实现。

比如Go语言里面内置了一个HTTP的库,包括 Server 和Client。这样的一个好处也是衍生的,我并不清楚她们的设计者可能预期没预期到这么一个结果。很多时候拿Go写的程序它不需要搞个Nginx去Save,基本上一个文件就可以把自己所有的功能全部融入进去。甚至有点天方夜谭的是各位读者可能现在觉得有些诡异。如果大家做过网站的开发或运维就会发现,网站的开发或运维有很多松松散散的文件,首先源码文件、Template文件、JavaScript、Images和CSS等一大堆乱七八糟的文件。但是拿Go的一些外部框架去写会发现很神奇的一点,Go语言可以把所有的文件整合成一个文件。这个拷贝到任意地方这个网站直接运行该文件都可以启动成功。连Nginx都不需要,就是因为它内置了一个HTTP的库,性能而且还不错。对比Nginx可能还有一些差距,但是它属于第一阵营的吧,还是比较不错的。

当然,可能有些读者会有疑问,这个丰富的标准库和前面所讲到的动态库有什么区别呢?这个标准库意思是说Go是静态编译的,它会把你用到的这些标准库给他编到Go的执行文件里,这是它的标准库。而前面说到的动态库它是Linux构成的一个基础,一般存在于系统层面的东西。可以通过yum或等方式进行安装,也可以自己去带。动态库一般是只提供了一种动态链接的一个so,如果你的二进制编完了,就可以直接链上去用。当然,你在编的时候C/C++要有很多头文件去指引着它去用这个东西。

那么动态库和静态库又有什么区别呢?作为一个用户来讲,静态库不需要去管,因为这个东西全部包进去了。动态库是需要满足它的动态库的各种依赖、版本的要求才能去使用。动态库可以自己带so,如果不带so要依赖于目标部署机器。

静态库在系统上存在都是.a的扩展名,这些库它是在编译的时候才会链进去,如图4所示。

图4

而动态库它是.so的扩展名,如图5所示。

图5

静态库只是在编译时、链接时链进去的,对于Go语言来讲,它相当于它内置的全是静态库,当然它并不是用的图4中的那种格式,有兴趣的话可以再去file一下图4中的链接,它会告诉你它是一个静态库,代码如下所示。current ar archive的意思就是静态库。

[root@golang ~]# file /usr/lib64/libbsd-compat.a
/usr/lib64/libbsd-compat.a: current ar archive
 

在Windows系统上动态库就是Dll文件,而静态库我们是看不到的,因为Windows不会把静态库放到我们家用的系统版本中。它只会放到开发的那种系统上。

Go语言开发环境安装

2.1 安装Windows版Golang SDK

现在我就来配置在Go语言在Windows系统下的环境,首先我们需要去官网下载Golang的安装程序,下载地址如下所示。。打开该下载地址会看到如图6所示的界面。

图6

目前,最新的稳定版本是1.11.2,我们只需下载 go1.11.2.windows-amd64.msi 这个安装包即可。双击鼠标左键打开Golang的支持库,打开后会看到如图7所示的安装界面。

图7

一直单击鼠标左键选择Next,当出现如图8所示的界面时,可选择自定义的路径,此处作为默认不更改。

图8

继续单击鼠标左键选择Next按钮,在单击Install按钮即可进行安装,紧接着就可以看到Go语言正处于安装中。安装执行完毕后会出现如图9所示的界面,单击Finish按钮即可。

图9

我们已经成功安装了Go语言,但是还需要进行一下验证,按住键盘上的Win+R键会打开运行程序窗口,在运行程序窗口输入cmd并按Enter键进入cmd命令窗口,直接在cmd命令行窗口键入go字符,如果出现如图10所示的命令就表示Go语言环境配置成功了。

图10

2.2 在Windows系统中安装Go语言 IDE Goland

打开Jetbrains的官方网站,在Jetbrains的官网上下载我们Go语言开发所需要的IDE Goland。Goland是一款非常强大的IDE,我们在它的基础之上就可以轻松并愉快的开发Go语言程序。

Goland下载链接地址:。

下载完Goland.exe可执行程序后双击打开goland-2018.2.4,启动后会看到如图11所示的安装界面。

图11

单击Next按钮进行下一步,会让我们选择该程序的安装路径,默认即可(或自定义),单击Next按钮初选如图12所示的界面,选择64位和关联.go文件。

图12

单击Next按钮下一步,Install下一步,然后就开始安装Goland IDE了,安装完成后单击Finsh按钮即可完成安装,和前面安装Go语言SDK步骤类似。

Goland安装成功后,会在桌面上显示一个JetBrains GoLand 2018.2.4的快捷方式。双击该软件启动Goland,它会提示如图13所示的界面,默认我们选择不去配置它(Do not import settings)。

图13

单击OK按钮它会让我们输入注册码,如图14所示。

图14

最上面的Activate表示激活,右边的Evaluate for free表示评估此软件,俗称试用。此处笔者选择Activate激活它,下面有三个选项。第一个表示jetbrains账户,第二个表示激活码,第三个表示Lincense 服务器。笔者此处选择第二个选项,使用从Jetbrains购买的激活码进行激活,如图15所示。

图15

单机OK按钮即可启动Goland IDE,界面Jetbrains设计的也非常的炫酷,如图16所示。

图16

启动成功后,先带领大家简单做一下测试。选择New Project,首先是选择代码的安放路径,其次是选择Go语言的SDK,也就是我们第一节所配置Go语言SDK,选择后,如图17所示。

图17

点击Create按钮完成项目的创建,如图18所示。

图18

选中01.Go语言魅力初体验目录,右键新建一个Directory(目录),名字叫做Hello Golang,新建成功后会在01.Go语言魅力初体验目录下面显示一个Hello Golang的目录,而且在对应的目录下面也会显示我们刚刚创建的这个Hello Golang目录,如图19所示。

图19

在Hello Golang目录下面去创建一个叫做Hello World的Go File(Go文件),但是提示我们需要设置Go的环境变量,如图20所示。

图20

这个GO PATH就是我们在第一节中安装Golang SDK时所选择的路径,我默认选择的是C:\Go,单机右边的“+”号按钮进行添加Golang的环境变量,如图21所示。

图21

单击OK按钮后你会发现Goland已经不再提示让我们设置GO PATH了。现在我们还需要在配置一下Goland,按住Ctrl+Alt+S键打开我们的Settings。点击Editor选项,选中Font设置我们的字体大小。字体大小可根据显示器大小进行设置,此处设置为22,字体个人比较喜欢Consolas,故设置成Consolas。然而下面的Color Scheme选项也可以进行一些个性化的设置,比如找到Go,我们可以设置Golang一些相关的高亮显示,如图22所示。

图22

下面的Code Style可设置缩进等等相关的,默认配置的就是4个空格表示一个缩进。

Version Control可设置登陆Github,如图23所示选择输入github的账号和密码即可完成登陆,登陆成功后显示如图24所示的界面。

图23

图24

全部设置完毕后,我们单击Apply按钮进行应用,应用成功后单机OK按钮即可退出Goland的设置界面,此时你会发现代码的颜色已经由白色背景了黑色背景,而且代码的颜色也随之发生了改变。

接下来我们写一个Hello World的程序,因为这是每门编程语言最开始都要写的一段代码,让这段代码来向世界讲述着它的来临,代码格式如下所示。

package main
 
import "fmt"
 
func main() {
 fmt.Print("Hello World")
}
 

右键选中我们的Hello World.go文件,选择Run Go build Hello World,或者按快捷键Ctrl+Shift+F10运行,运行结果如图25所示。

图25

关于Golang的Hello World每个语法是干什么用的,本节暂不做解释,随着我们后面的学习我相信各位读者会慢慢了解的!

2.3 配置Windows系统Lite IDE

本节我们来使用下Lite IDE,它属于轻量级的Go语言IDE,它可以把Go语言变成EXE。Lite DIE可在本书所提供的Gitlab上进行下载,下载地址如下所示。

下载完成后进行解压操作,解压后将Lite IDE拷贝到Go语言环境安装目录的根目录下,然后将C:\Go\liteidex35.2.windows-qt5.9.5\liteide\bin目录的liteide.exe启动程序发送到桌面,回到桌面启动Lite IDE启动程序,启动成功后显示如图26所示的安装界面。

图26

选择左上角的文件,点击新建,或者按快捷键Ctrl+N键进行快捷创建,我们选择第二个Go Source File,路径我们可以在01.Go语言魅力初体验新建一个叫做Lite HelloWorld 的目录,文件名叫做LiteHelloWorld,最后单击OK按钮即可,如图27所示。

图27

注意:如果弹出对话框问我们是否建立加载,我们选择YES即可。

这个Lite IDE也比较好用,我刚创建的Hello World会自动加载代码,如图28所示。

图28

选择顶部菜单栏中的编译,选择BuildAndRun进行编译并运行,结果如图29所示。

图29

所以,到此为止,Lite IDE我们就配置成功了。我们打开01.Go语言魅力初体验\LiteHelloWorld目录会看到下面有一个LiteHelloWorld.exe的可执行程序。这个可执行程序就是我们刚才把Go语言的程序变成了EXE的可执行文件程序。

打开cmd命令窗口,进入到01.Go语言魅力初体验\LiteHelloWorld目录下把LiteHelloWorld.exe复制到cmd命令窗口按Enter键就可以运行了,如图30所示。

图30

2.4 配置Linux系统下的Go语言开发环境

Go语言环境在Linux操作系统下安装也是非常方便、非常简单的。首先我们需要准备一台可以实现Golang环境的云服务器、物理服务器或者本地的虚拟机。本节所演示的操作系统是基于CentOS/RedHat 7.X版本进行讲解的。本节使用的是 阿里云 的云服务器。首先我们无论使用何种形式,都需要使用我们的远程终端连接到我们的远端服务器上。

首先我们来安装一下Go语言的二进制包,下载地址:。下载go1.11.2.linux-amd64.tar.gz该二进制软件包到远端服务器上,下载后并进行解压操作,命令如下所示。

[root@golang src]# wget

[root@golang src]# ls

go1.11.2.linux-amd64.tar.gz

[root@golang src]# tar xf go1.11.2.linux-amd64.tar.gz

[root@golang src]# ls

go go1.11.2.linux-amd64.tar.gz

运行go version命令查看go版本时提示报错-bash: go: command not found,这是因为没有设置系统环境变量造成的,只需在家目录下面的.bash_profile文件中添加如下所示代码即可。

[root@golang ~]# vim .bash_profile
#根目录
export GOROOT=/usr/local/go
#bin目录
export GOBIN=$GOROOT/bin
#工作目录
export GOPATH=/data/www/golang
export PATH=$PATH:$GOPATH:$GOBIN:$GOROOT
 

设置完系统环境变量后在使用go version命令查看golang的版本已经是可以了,命令如下所示。

[root@golang ~]# go version
go version go1.11.2 linux/amd64
 

输入go env也可以查看Go语言的一些系统相关的配置信息,命令如下所示。

[root@golang ~]# go env
GOARCH="amd64"
GOBIN="/usr/local/go/bin"
GOCACHE="/root/.cache/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/data/www/golang"
GOPROXY=""
GORACE=""
GOROOT="/usr/local/go"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build717386972=/tmp/go-build -gno-record-gcc-switches"
 

现在,我们来一起运行一个测试示例测试一下是否安装正常,代码如下所示。

[root@golang www]# cd
[root@golang ~]# cd /data/www/humingzhe.com/
[root@golang humingzhe.com]# vim hello.go
Press ENTER or type command to continue
package main
 
import "fmt"
 
func main() {
 fmt.Println("Hello World")
}
[root@golang humingzhe.com]# go build hello.go
[root@golang humingzhe.com]# ./hello
Hello World
 

2.5 配置Linux系统下Go语言IDE Goland

待续

2.6 配置Mac系统下安装Golang开发环境

待续

3. 在Github上创建Hello World

在前面的章节中,我给同学们讲解了Golang的特性,Golang在各种系统环境下的IDE的使用以及简单的写了一个Hello World的小例子。通过简单的Hello World简单了介绍了下Golang。下面我们来介绍下Hello World中每段代码的含义,代码如下所示。

package main 
import "fmt" 
 
func main() {
 fmt.Print("Hello World")
}
 

第一行代码package main:表示main包名。golang中它有一个特性,它是用package来组织源代码中各种各样的东西的。你可以去把很多代码写到不同的源文件中,但是你如果声明成同一个package就相当于是一个package里面的东西,和Java中的Class是很相似的一个概念。

第二行代码import “fmt”:表示引用包的格式。golang不像其他语言一样,它是通过直接写一个fmt.xxx来表示的,而且必须用引号引起来,它是当成字符串一样去引用。Golang的奇葩之处在于它的引用是用绝对路径引用,后续的章节中会涉及到这个问题。假设引用github上的一个包,必须要引用import “github.com/humingzhe/golang”。

第三行代码 func main():Golang所有的开源项目,无论是大项目还是小项目都要有main函数,main函数是从C语言中继承过来的。main函数作为程序的入口点,它编译后程序运行时会找main函数编译出来的地方去从头到尾开始往下运行。main函数的名字不能乱用。

第四行代码fmt.Println(“”):Golang里面其实也带了一个最简单的print(“”)函数,但是我并不建议大家使用,Go官方也不建议使用print(“”)函数。这个print(“”)函数是用来做golang初始版本go的debug用的。如果你想print什么东西可以使用fmt包里的Println。fmt是format的简写,通过符号点接上Println。它的函数P必须是大写的,ln表示换行,在括号里面加上字符串就可以了。如果不加ln的话是不会换行的,不妨我们来测试一下,测试结果如图31所示。

图31

第五行代码:Println表示打印Hello World并换行,Println和print不太一样,Println默认是打到标准输出上的,print是打到标准错误上的。为什么要有print这么一个东西?假设Println在实现的过程中,比如我实现Println,我想报个错怎么办?或者说我不想依赖这个包做些底层的东西它是通过print来实现的。print可能会在后续的版本中移除掉。所以禁止大家使用。讲解这个print只是告诉大家看到print是干什么用的就可以了。

Println有很多的兄弟姐妹,通过fmt.就可以看到很多兄弟姐妹。如果你熟悉C/C++编程会发现基本上就是系统调用的直接翻译,如图32所示。

图32

下面简单介绍下里面的几个函数,Print家族有Println和Printf,前面我们讲解过了Println,现在来介绍下Printf。Printf(“”)表示支持一个format string,什么是format spring呢?就是先指定格式,比如先指定%s,比如%saaa%s。然后在指定需要打印什么,比如打印”hello”,”golang”。它会打印出来helloaaagolang,代码如下所示。

package main
import "fmt"
func main() {
 fmt.Printf("%saaa%s", "hello", "golang")
}
 

%s表示做替换,基本上是所有语言都支持的一个特性,为什么说它是所有语言都支持的的特性呢?因为这是世界标准组织ANSI规定的标准。它是一个比语言更高级的东西,所以说很多语言都支持它。%s表示字符串,%d表示数字,不妨我们来测试下%d,代码如下所示。

package main
import "fmt"
func main() {
 fmt.Printf("%daaa%s", 123, "golang \n")
 fmt.Printf("%d111%d", 123, 345)
}
 

打印的结果同学们可以先想一下会是什么,是不是123aaagolang和123111345两行结果呢?如图1-33所示和我前面提到的结果完全一致。

图33

Go有两个非常重要的环境变量,GOROOT和GOPATH,GOROOT环境变量指向的是GO的安装路径,如图34所示。

图34

ll /usr/local/go/是安装go包的目录,一般看目标路径都是指向到bin目录。bin目录里面有go的运行程序。把GOROOT里面的bin目录加入到PATH(系统环境变量)中。GOPATH是以后源代码放到这个目录中。

ls -l $GOPATH中的bin里面的东西相当于是用go get安装的一些东西会放到这里面,pkg是用go get去get一些pkg放到这里面,src是我们自己开发的源代码的存放目录。比如src里面有很多的目录,比如这个github.com目录中存放的就是github上面的一些东西,而wangzhiqiang这个目录中存放的是我自己的一些项目源代码。

我们还要做一件非常重要的事情,那就是在github上面去创建一个项目,因为我们以后开发的项目都会放到github上去托管。现在带着大家来体验下github。Go语言它天然的和代码管理仓库git有着非常密切的一种关系的。所以说,大家如果没有github账号的赶紧去注册一个github账号。有github账号的,就可以进行登陆了。登陆后把github的账号名字(username)发给我,这个账号名字并不是注册登陆的邮箱,而是你的ID。把你的ID发给我,我去建一个group把同学们都加进来。

我创建好group后,同学们如何把我在github上创建的那个目录copy下来呢?可以使用go get命令进行下载,命令如下所示。

[root@golang ~]# go get -u github.com/HuiMingTenchnology/golang-homework

go get下来之后我们进入到GOPATH目录里面的src目录下面的github.com目录下面就可以找到我们刚下载的HuiMing Technology目录,命令如下所示。

[root@golang ~]# cd $GOPATH
[root@golang golang]# ls
bin pkg src
[root@golang golang]# cd src/github.com/HuiMingTenchnology/golang-homework/
[root@golang golang-homework]# ls
LICENSE README.md
 

现在,我们就把github中的这个组织checkout进来了。注意,我们项目的目录路径一定要搞成这个样子,千万不要把golang-homework/ check out到别的目录里面。前面的github.com/HuiMingTenchnology/目录千万不要省,省了就会出现问题。我觉得这是Golang里面最奇葩的一个地方,就是它必须要跟github或者repo上的路径保持一致,公司内部repo也是一样的,就是要把URL体现到路径里面。

项目check out下来后,就可以把hello world这段代码push到github的项目中了。如果你只是看的话,同学们可以只用这一个repo,如果后期做别的项目呢,可以去建自己的目录,在自己的目录中去做项目就可以了。当然还是用这一个repo。

在golang-homework这个目录里面增加一些内容。比如增加一个hello.go的文件,添加如下所示代码。

[root@golang golang-homework]# vim hello.go
package main
import "fmt"
func main(){
 fmt.Println("Hello World")
}
 

执行git add命令,将hello.go从工作区添加到暂存区中,命令如下所示。

[root@golang golang-homework]# git add hello.go 
 

将暂存区里的改动文件添加到本地的版本库,命令如下所示。

[root@golang golang-homework]# git commit -m "add hello.go"
[master 46ef2c5] add hello.go
1 file changed, 7 insertions(+)
create mode 100644 hello.go
 

最后push到github中,命令如下所示。

[root@golang golang-homework]# git push -u origin master
Username for ' 
 

提示让我们输入username,什么意思呢?。默认使用go get它有一个好处,就是它会自动目录建起来。但是有个坏处就是默认会通过https这种只读方式帮你把repo给check out下来。这时我们其实不想这样做,因为我们想编辑它,想用SSH这种形式。我们可以编辑.git目录下的config配置文件进行更改,把里面的url改成github中的git clone 以.git结尾的路径。修改如下所示。

[root@golang golang-homework]# vim .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = git@github.com:HuiMingTechnology/golang-homework.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
 remote = origin
 merge = refs/heads/master
 

我们再次执行git push命令就可以了,命令如下所示。

[root@golang golang-homework]# git push -u origin master 
 

打开github我们可以看到,在我们这个golang-homework项目中已经存在这个hello.go文件了,如图35所示。

图35

编写HTTP/TCP版Hello World

先来看下下面这段代码。下面这个版本是http版的一个Hello World的写法。任何外部的库都没有使用,就是通过这么几行代码就可以实现一个简单的HTTP服务器。它可以访问根目录,访问Hello World。而且这个HTTP服务器写出来之后性能会比你想象中要强很多。具体强在哪里呢?先别着急,后面我会带着大家去做一个简单的性能测试。

package main
 
 import (
 "fmt"
 "net/http"
 )
 
 func handler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "Hello World")
 }
 func main() {
 http.HandleFunc("/", handler)
 http.ListenAndServe(":8080",  nil )
 }
 

现在我们使用goland ide把我们前面章节中创建的github项目克隆到goland ide中,在goland ide中找到文件选择settings打开,选择gopath,设置我们的gopath路径,如图36所示。

图36

将github项目存放到F盘的golang目录下的src目录中,若没有src目录则需要自行创建。创建完毕后,我们找到下面的Git,配置下我们的git,配置如图37所示。

图37

当然,github需要进行登录才可以使用。不然是拉取不下来源码的。当上面配置完毕后,我们把设置界面关闭掉,找到goland ide顶部菜单栏中的VSC,选择Check out from Version Control中的Git进行配置,设置完毕后单击右侧的Test按钮进行测试,测试无误后就可以点击下面的Clone按钮进行克隆了,如图38所示。

图38

克隆成功后,就可以在goland ide中显示github中的项目了,如图39所示。

图39

在根目录下新建一个叫做http-hello的go文件。http-hello.go文件新建成功后它会自动帮我们写入package main这行代码。它和我们之前写的hello.go文件有些不同,首先第一行代码都是package main,必须要有这么一个main的包。然后需要import导入包,但是http-hello.go文件中我们需要import两个包,可以写两行,比如import net/http,import fmt。当然也可以进行简写。就是import用()括起来在()里面写”fmt”和”net/http”,最后把这两个引用进来就可以了。代码如下所示。

package main
 
import (
 "fmt"
 "net/http"
 )
 

在Go语言中写一个简单的http server它是一个非常范式化的东西。什么意思呢?首先我们要有一个main函数,main函数是一个入口。然后直接用http包里面的handler function注册一下,就是说我访问httpserver的根目录我就用这个handler去执行,这个handler的格式也是写死的规定的一种格式。就是要有一个handler,第一个参数是w http.ResponseWriter 第二个参数是r *http.Request。下面是把你要输出的内容写到w里面。这个w的类型是http.ResponseWriter。在这里面Go有一个特点,只跟Java或者C相比较的话,他的特点是这个参数的类型放到参数后面去写。我只是觉得这个事情挺怪异的,但是人家这么设计咱也是没有办法的。放后面就是说明我这个参数叫w,类型必须是http.ResponseWriter。r是http.Request,把这个函数做一个参数,在Go里面不区分函数和函数指针,它是一样的东西。然后就把这个handler直接就注册到这个上面,然后在直接执行http.ListenAndServe,然后传个nil就可以了,这是一些参数相关的东西,这个里头我们不在用它。这就是告诉他我们监听这个机器所有的Interface的8080端口就可以了。这个时候去访问这台机器的8080端口,他就会输出一个Hello World,代码如下所示。

func handler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "Hello %s!", r.URL.Path)
}
 
func main() {
 http.HandleFunc("/", handler)
 http.ListenAndServe(":8080", nil)
}
 

它是一个什么流程呢?这里面ListenAndServe相当于启一个服务器,启一个服务器之后,这个服务器就会根据你注册的这些回调函数,当一个请求过来之后,它就会主动的把这些ResponseWriter这个w给你传进来,Request也给你传进来。你可以根据request进行一个解析,看她请求的是什么东西或者参数是什么都可以在这里面获取到。获取到之后,你所需要干的唯一一件事就是往w里面写一点东西就可以了。

Fprintf和前面说到的printf是类似的。只是Fprintf是接收一个writer的一个参数。就是把Hello World这个字符串写到writer里面就可以了,以上就是一个完整的HTTP服务器的执行处理过程。

运行一下这个HTTP服务器,在goland ide中右键点击run,如图40所示。

图40

打开浏览器,在地址栏中输入本地IP地址127.0.0.1或者localhost和HTTP服务器的8080端口即可进行访问,如图41所示。

图41

登陆远端服务器,在远端服务器上git pull下来github上我们刚刚提交的http-hello.go文件,命令如下所示

[root@golang golang-homework]# git pull
[root@golang golang-homework]# ls
hello.go http-hello.go LICENSE README.md
 

使用go build命令去build http-hello.go这个文件,build成功后使用ls命令可以看到多出来一个http-hello的文件。把http-hello文件运行起来,命令如下所示。

[root@golang golang-homework]# go build http-hello.go

[root@golang golang-homework]# ls

hello.go http-hello http-hello.go LICENSE README.md

[root@golang golang-homework]# ./http-hello

现在来做一下压力测试。使用siege命令进行压力测试。在压测http-hello之前我们先压一下Python版的看能压到多少,命令如下所示。

[root@golang golang-homework]# python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...
 

重新打开一个终端窗口,使用siege命令进行压力测试,命令如下所示。

[root@golang ~]# siege -c 200 -t 5s 
 

压出来的结果是2216.53 trans/sec,如图42所示。

图42

接下来,我们再来测试下Go版本的http-hello。在第一个终端窗口执行./http-hello,然后继续在第二个终端窗口执行压测命令,命令如下所示。压测结果如图43所示。

[root@golang ~]# siege -c 200 -t 5s 
 

图43

我们可以从上面看出来,Go还是要比Python性能强大很多,一个在3000左右一个在13000左右。基本上并发提高,Transaction rate就可以提高。其实瓶颈应该在siege这边,你只要把并发提高,他的qbs就会上升,这就可以说明Go的性能完全不是问题。

http-hello这个文件大概是6M左右的大小,命令如下所示。

[root@golang golang-homework]# ll http-hello -h
-rwxr-xr-x 1 root root 6.3M Dec 4 21:40 http-hello
 

就算是一个最简单的hello.go文件也会有1.9M,为什么会这么大呢?其实和Java比起来也不算大。但是和C编出来的比较可能显得比较大。它这里面相当于是把Go的很多的库或者其它的一些东西都加进去了。包括里面和协程相关的各种各样的东西,所以说编出来一个最小的文件大概也要1.9M左右。

如果用C去写一个带HTTP服务的东西其实也差不多啦,也要拉一堆库进来,但是Go全都内置了,大概就是这样的一个情况。

我们还可以继续把http-hello.go文件中的内容进行改造,代码如下所示。

package main
import (
 "fmt"
 "net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "Hello %s!", r.URL.Path)
}
func main() {
 http.HandleFunc("/", handler)
 http.ListenAndServe(":8080", nil)
}
 

在func handler里面传了一个参数r,而且我们这是Fprintf。注意分析,这个函数命名其实是一个非常通用的,其它的语言都会有这种函数。这个大写的F表示file的意思。也就是它往一个句柄里面打印,然后print,后面的小f表示format的意思。也就是往一个句柄里面按一定格式去打印。这个格式就是用这个格式,然后%s,就是把urlpath打印出来。什么意思呢?就是我在url里面打点什么东西网页上就会出什么东西。如图44所示。

图44

但是我在url中写入aaa=?1u2ieas,?1u2ieas他就不会进行打印,因为这是属于参数的。如图45所示。

图45

但是当我们去掉r.URL后面的.Path时再次刷新页面就可以进行访问了,如图46所示。

图46

这个URL里面应该是有一个string函数什么的,他就可以把后面的东西全部加进来。

我们可以改造一个自己的程序,我们的目标是什么呢?我们通过这个东西打出来请求机器的IP。我没看,但是我知道这个r.里面应该可以.出来有一个东西就是表示他请求的端的IP的。然后我们现在的任务就是http-hello这个程序改成请求打印hello这个ip。大家发挥一下聪明才智看看怎么可以搞出来。

其实r.RemoteAddr这个参数就是,如图47所示。只不过有时也会打印出来ipv6的格式,如图48所示。

图47

图48

当然,我们还可以继续修改,比如想有不同的业务逻辑或者等其它的内容,都可以用不同的handler去处理,代码如下所示。

package main
 
 import (
 "fmt"
 "net/http"
 )
 
 func handler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "Hello %s!", r.URL)
 }
 
 func admin_handler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "admin %s!", r.URL)
 }
 
 func main() {
 http.HandleFunc("/", handler)
 http.HandleFunc("/admin", admin_handler)
 http.ListenAndServe(":8080", nil)
}
 

我们当访问localhost:8080的时候默认访问的是根目录下的Hello,但是当我们在/后面加上admin的时候访问的则是/admin页面下的内容admin,这相当于是一种匹配的逻辑,匹配到什么就是什么。如果大家懂Web开发的话相信会都会明白和理解这个道理,比如我们访问/admin,就会在浏览器中打印出来admin /admin,结果如图49所示。

图49

不妨在/admin后面再加上一些内容,比如/admin/aaaabbshdhas,如图50所示。

图50

同学们发现没发现,他居然没往/admin上面去匹配,而且匹配到的/目录下,这是为什么呢?其实这是因为我们的代码/admin后面没有加上/造成的,我们在/admin字符串后面加上/即可,代码如下所示。

func main() {
 http.HandleFunc("/", handler)
 http.HandleFunc("/admin/", admin_handler)
 http.ListenAndServe(":8080", nil)
}
 

此时再去访问浏览器,则会发现它显示的是admin/admin/ aaaabbshdhas,如图51所示。

图51

但是在将/admin/改成/admins/它就会像图50一样,访问的还是根目录下的hello,如图52所示。

图52

它会按照匹配顺序先匹配长的在匹配短的,也就是先匹配/admin/在匹配/的一个结果。

这个来说也没什么特别的,Go语言它还是挺简单的,比如说连接各数据库,直接RESTFUL API就可以写了。而且简单到就连编译出来部署也是非常简单的。不知道你拿Go写的程序的人简直以为你给他编译了程序出来,更不知道你是付出了多大的辛苦,他会想是不是拿C/C++写的呀?那可能得写好几千行代码才能实现。而Go语言就纯傻瓜化了。

上面是一种最简单的,这里有一种办法来实现,新建一个叫做time-server.go的文件粘贴如下所示代码。下面的代码相当于是不用HTTP包,直接用TCP的方式来实现一个TCP的一个很底层的Sever。就是当我连上这个东西后,往这个连接里面去打一下当前的时间,在把这个连接关闭掉。

package main
 
 import (
 "fmt"
 "log"
 "net"
 "time"
)
 
func handle(conn net.Conn) {
 fmt.Fprintf(conn, "%s", time.Now().String())
 conn.Close()
}
func main() {
 l, err := net.Listen("tcp", ":8080")
 if err != nil {
 log.Fatal(err)
}
for {
 conn, err := l.Accept()
 if err != nil {
 log.Fatal(err)
 }
 go handle(conn)
 }
}
 

现在用8080端口去弄个了一个服务,虽然可以使用浏览器进行访问,但是它其实是一个TCP版的。最好是用telnet去访问比较好。

使用go run命令在Linux服务器上运行time-server.go文件,命令如下所示。当终端窗口的的光标停留在go run time-server.go下方不动时表示该程序已经处于运行状态。

[root@golang golang-homework]# go run time-server.go

重新打开一个远程终端窗口,使用Telnet命令ping本机IP的8080端口,这个Server的目标就是连接之后不用说话,这个TCP就说我不说东西,它直接给我回一个时间。然后就把我断开了,如图53所示。

图53

time-server.go里面的代码是一个典型的TCP服务的写法。同学们应该都明白TCP和HTTP之间的关系。就是我可以在TCP上写HTTP的协议就能实现一个HTTP的服务器。同理,HTTP是运行在TCP上面的。

现在带领同学们来解释下time-server.go文件中的每行代码的作用,第一行package main和下面的import “”此处不再进行讲解,前面已经讲解的非常深入了。我们直接从func处进行讲解,代码如下所示。

func handle(conn net.Conn) {
 fmt.Fprintf(conn, "%s", time.Now().String())
 conn.Close()
}
func main() {
 l, err := net.Listen("tcp", ":8080")
 if err != nil {
 log.Fatal(err)
 }
for {
 conn, err := l.Accept()
 if err != nil {
 log.Fatal(err)
 }
 go handle(conn)
 }
}
 

func handle表示声明一个handle函数,这个handle也比较简单,它传参只有一个conn,它相当于是往conn里面打一个time.Now().String()。就是取当前的时间转换成String然后传过来,直接往这个(conn)链接里面写,写完之后立马关闭掉这个连接(conn.Close)。

下面是声明了一个main函数,首先拿net.Listen去监听TCP的8080端口。然后判断一下是否有误,有错误就打印log.Fatal(err)。然后进入for循环,用L(Listen Socket)进行Accept(接收连接过来的连接),接收到连接(conn)后先判断一下有没有错,有错就打日志,没有错就把这个连接当成一个参数直接传到handle函数里面。

handle函数前面如果不加go也是可以的,就是相当于直接调用这个函数,它意思就是说我去调用一次这个函数。然后等这个函数返回了,我就让for循环结束。

上面示例代码中for循环的写法是直接一个for 然后什么都不加,这在golang里面表示无限循环的意思。和其他一些编程语言中的while ture是一个意思。

如果Accept异常呢?Accept的异常都是通过err(error)返回的,Golang里面不太推崇用异常来处理,都是通过返回值进行处理。Golang里面很多调用都是返回两个元素,比如像main函数中的L和err(error),for循环中的conn和一个err。很多都是这样的一种结构。就是一种东西返回两个。

但是最下面加了一个go就完全不一样了,什么意思?这个意思就是说,我去开一个协程,这个协程去单独运行这个函数handle,然后在这个协程函数里面去进行一个处理。

那么问题就来了,什么是协程呢?这个来说可能会偏理论一些。但这也是绕不开的。如果大家不清楚什么是协程,那么大家应该是清楚什么是进程的吧。除了进程还有 线程 和协程。总共是这么三个东西。这三个东西呢它相当于是一个层面的东西,是操作系统用来管理任务的一种方式。

Golang并发原理和C10K问题

进程很好理解,比如说去敲一个命令,或者去运行一个Server,这些Server和命令本身就是一个进程。这些进程里面可能会开很多个线程,然后进行一些操作。开线程的含义主要在于能充分利用多核,也可以把一些任务进行分工。协程是一种更加轻量级的东西,它比线程更要轻量级,轮重量来说,进程>线程>协程。论历史出现的顺序来讲,最先出现的是进程,这个毋庸置疑,为什么最先出现的是进程呢?因为稍微有点像样的操作系统都是多任务操作系统。既然是多任务,那你很多个任务之间如何去区分彼此呢?他就是靠进程,不同的进程去区分。进程完了出现的不是线程,而是协程,最后才出现的线程。它是这么一个历史过程。

协程具体又是个什么样的东西呢?我们说这个东西就不得不引入一个概念(话题)。我们从一个非常直观的角度上,我们运行一个Linux命令,或者写一个什么样的程序,我们在Linux里面有这么一个直观的 感受,就是说你运行一个非常蹩脚的程序,它锁死了,或者它把CPU打满了,或者它吃了无数的内存,或者它占了无数的句柄,或者说打开无数的文件,无数个无数,不管是为什么,只要给他kill -9,它基本就可以完全消失的无影无踪。kill -9之后,这个进程无论是多么恶心,它占了多少内存,多少CPU,它都如数的给你归还到你的手中。它不存在我这个进程被kill -9死了之后,我占的内存和CPU还释放不掉,这种概念是不存在的。所以,由此得出,一般教科书上会讲,进程是什么,进程是操作系统管理资源的最小单位,就是说不同的操作系统,无论是Linux、Windows还是iOS还是Android这些东西,它都有这种概念,他是一种非常广泛的概念,这种概念用来管理这件事情的。无论哪个操作系统它都要实现出一套接口,能让我以一种方式去把我的所有肮脏事情全放到里面,而且操作系统要有这个能力,你在这个范围内做出的任何事情,任何对CPU内存的一些占用或者其他的东西,他都能一股脑的会被操作系统一直追溯着。它一旦把你干掉之后,它能够把你的债全部要回来。

线程就不一样了,我们应该从直观的感受上去讲,比如说一个程序它开了一百个线程,其实如果这个程序不提供什么样的机制让你能控制它有多少个线程的话,你其实根本控制不了它的什么。人家开个线程,你也关不了这个线程。它没有一些可以操作的机制,线程一般来说是每个程序或者每段代码或者每个进程它去组织自己的调用的这种方式或者协作的方式的一种内部的方式。但是它由于是一种内部的方式,所以说,它和进程又有很多相同类似的点。就是说每个线程他都能去占一个CPU,假设你有一个四核的CPU,你开一个进程,每个进程可以占一个CPU,一个进程相当于里面就包含了自己的一个线程。或者换一句话说,进程类似于我们是一个班,我们是Golang班,这个班作为一个整体,无论我们这个班是有一个学生还是一百个学生,它都是一个班。我们这个班结束了,毕业了,我们就全体结束了。无论你是一个人还是两个人。但是呢,如果这个班里面有四个学生,我就可以像分神一样,这四个学生来虽然来说是可以共享内存,但是我可以分别占到每个CPU上去独立的进行一个调度。类似于一个身体多个脑袋的结构。多个脑袋可以同时运行,而且互相分工,你无论是说每个脑袋去做流水线这样的步骤还是说就直接把任务分成四份去做,这都是你内部的事情,就是这样的一个东西。这是线程当时被创造出来时的一个目标,目标就是充分利用多核,所以说当时大多数操作系统实现线程的时候它偷了个懒儿。特别是Linux操作系统,它偷了一个非常大的懒儿。它一想,那我进程和线程,仔细想一想,从内核实现者这个角度上想,好像没有太大的区别。 我一个进程也能占一个CPU,一个线程也能占一个CPU,只是来说这一堆线程也可以叫做一个进程。既然他们从CPU占用上是一个,那其实操作系统来说,管的最复杂的一件事就是在CPU上调度任务。这段代码也是可以公用的,唯一的不同就是两个进程它一定是内存是独立的,我这个进程怎么写,怎么崩溃写,写的再烂,我顶多会是挤占CPU的时间,不可能是把另外一个程序搞崩溃。除非把内存挤得OM了或者挤得根本跑不动了,是有这种可能的。但是你里面写的再烂也干扰不了别人的程序。

进程和线程对操作系统来说,最大的一个差异就是说他们内存共享不共享,两个进程不共享,同一进程里的两个线程它是共享内存的。类似于一个班级中一屋子的人负责完成一幅画,每个人画一个角度,画一块,都去画画。这相当于是我们共享一个画布,或者说一块去写一个字,你写一撇,我写一捺,还是说我们分别去写不同的字,这都是无所谓的,但是呢,只有在这一个地方上,如果说我是多个进程就类似于我给你们不同的画布,你画布搞成多么恶心都和我无关。就是这样的一个区别。所以说,你在Linux上去看,它的线程ID和进程ID它用的是同样的空间。不知道你们有没有这样的体会。

什么意思呢?在Linux服务器上使用ps aux命令可以看到如图54所示的结果。

图54

图1-53

我们可以看下nginx,nginx它是一个多线程的。我们可以从图中看到nginx它有一个进程,使用pstress命令+nginx的PID可以看nginx开了多少个线程,命令如下所示。

[root@golang ~]# pstree -a 15514
 nginx
 ├─nginx
 ├─nginx
 ├─nginx
 └─nginx
 

那怎么把线程的PID打印出来呢?使用pstree命令同样可以实现打印出来nginx线程的PID,命令如下所示。

[root@golang ~]# pstree -pa 15514
 nginx,15514
 ├─nginx,15515
 ├─nginx,15516
 ├─nginx,15517
 └─nginx,15518
 

15514相当于是进程,使用ps aux命令只能看到15514这个PID。但是使用pstree可以看到线程的ID。说是线程,但为什么能看到这线程的数字感觉和兄弟姐妹似的。其实它是共享一个线程的空间,如果你手里有Mac的话,可以去看下Mac系统下每个线程不会占用进程PID的空间。它里面给线程分的ID是另外的一组ID。只是存在这一点点差异。在调度上或者很多写程序的角度上Mac(Unix)的线程和Linux的线程没有太大的差异。所以说,很多人就会说线程跟进程没有本质上的区别。比如我们知道在Linux系统上有个系统调用叫做forrk,一般我们开线程都是要pthread-create去调用。但是如果你了解Linux来说,Linux系统中只有一个API,叫做clone,它只是通过调整clone这个系统调用的不同参数去创建线程和进程,其实如果你有能力的话可以创建出介于线程和进程中间的一种东西也是可以的。我现在说的都是简化版,其实是有一些完整的文档里面会和你讲线程和进程它那些是共享的哪些是不共享的。比如线程之间他是共享FD的,就是说我在线程A里面打开一个文件,它FD给我返回一个句柄是3,那我在线程B里面直接拿3,他打开我去用这都是可以的。或者更直观的,我多个线程都往console上去标准输出应该是1,FD是1。我多个线程往console上打的时候其实是共享的这一个FD去往console上打。然后就是FD是共享的,内存是共享的,信号处理也是共享的。所以,你会有一个非常直观的感受,你kill一个nginx,无论kill这些线程哪个pid,kill -9都会全死掉。

因为从操作系统这个角度,它不知道怎么弄,因为线程是你写程序的人控制的。你让我kill掉它,我不知道你里面怎么共享的,所以说,我就只能一股脑的给你们一家子全部干掉。就好比古代的黄帝有权利诛九族一样,和你相关的全部给你干掉。这是进程和线程,这是一个非常基础的知识,然后还有很多的基础知识,它们共享不共享,但是其实最主要的就是这些东西。然后就出现了一个问题了,早年线程创造出来是为了充分利用CPU的,但是后来被用烂了,为什么被用烂了,就是Linux这个操作系统被开发出来的时候,我们都知道Linux的前身它是仿照着Unix在贝尔实验室创造出来的。后来又在加州大学伯克利分校被发扬光大。美国人嘛,他们当时没觉得世界上有这么多人,或者说也没想到世界上会有这么多人会上网,当时他们就在实验室里有一台电脑,无数人挤在一台电脑前,当时的电脑非常昂贵,不是谁都可以买得起的,不像现在一样,人人都可以买一台电脑。所以,他们就觉得网络连接这种事情,一般他们也就可能困掉几个socket这种情况,因为ssh上去一个socket,我去收个邮件一个socket,她们觉得基本上够用了。所以说,在这里头因为这种假设,所以给Linux挖了无数的坑,你们想想有哪些坑?给你们提出来几个,不知道你们有没有印象。你们用linux肯定知道Linux的服务器上线之前要改一个东西,叫做ulimit -n。你有没有想过,Linux的这帮设计者是傻逼吗?为什么设那么低。因为它是有历史原因的。就是Linux的各大发行版,因为历史的原因觉得1024绝对够用了。哪来的这么多FD,但是事实上就是这种C10K问题,就是说一万并发问题到了九几年的时候才被遇到。突然发现一个服务器可能真会有一万个人并发来访问,然后这个事就很操蛋了。所以说ulimit -n要打开。还有非常经典的就是IP地址, IPV4 ,大家都知道IPV4其实当时那些人设计的时候都已经想破天了,IPV4地址空间有多少?65535*65535这么大的空间,大概是40(4294836225)亿的空间。那些人当时想,这些空间够全人类用一辈子甚至几辈子都用不完吧。谁曾想现在一个人不知道能占多少个IP,一个平板,一部手机,一台服务器,甚至手表,电视,扫地机器人都用到IP。还有一个坑就是早期大家有没有用过一个web服务器squid,原来是做web代理的,做缓存的一个程序,现在基本上不用了,因为这东西从性能上很烂的。当时网上流传着一个东西,就是告诉你编squid之前,make之前你要去改/usr/local/include里面有个limit.h头文件里面有个1024要改大点。内个东西的参数是什么呢?内个东西的参数一个系统调用,用来做IO复用的select调用,select当时设计的时候它只能处理FD号小于1024的这些,后来被证实这个东西太不够用了。所以,在后来就出现了epoll。这就是当年考虑不周的一个问题。还有一个问题就是线程这个问题当时也考虑的不周。线程为什么考虑不周呢?因为当时觉得我一个机器当时大多都是1个CPU,每个人写一个程序就完了,顶多你写俩线程我觉得就是太洋气了。后来,由于连接多了,IO的好多接口是阻塞的,阻塞就导致你写程序时这种最简单的逻辑,就是说我开一个线程去盯一个连接。这个连接他有可能很慢,因为CPU运算速度比网络要快得多。但是没有办法,我为了应付一个连接就要开一个线程,应付一个连接开一个线程…,当时很多很多的服务器都是这种模式,apache早期,甚至Apache2也是这种模式,就是开线程就卡。还有一个叫做varnish的东西不知道大家有没有用过,他也是一个反向代理服务,varnish是典型的靠开线程硬抗连接的。来多个连接开多少个线程。当时我线上也用到过,varnish开几千个线程是非常常见的。开几千个线程啊,开几千个线程意味着什么呢?如果你CPU非常多,16核,几千个线程抢16个核,它会来回切,来回切,来回切。就类似于几千个人去抢四个厕所一样。你上完厕所出来了,我再去厕所。我上厕所出来了,他在上厕所。它因为很重,所以它只能这种厕所,而不可能是说一下进去四个人,大家对这池子一起尿,这种是线程不允许的事情。因为它的调度当时设计的非常重,他和进程用的是一样的调度模式,所以说,为了应付这件事,就有人在很早的时候发明了一个东西叫做协程。协程这个东西它类似于操作系统的很像后门的一种东西,他长得很古怪,他像是给你一套api能让你主动的去保存你的这个任务运行的状态。然后你可以在去用一些东西恢复这个状态。这样的话,你在一个进程,甚至一个进程的一个线程里面去模拟出多个任务,我可以先干干这个任务,然后把它扔到后台保存起来然后再去干其他的任务,然后在扔到后台保存起来。因为你自己控制,你自己知道要保存哪些东西,所以说,他切换的力度就非常非常小,他不像CPU去调度线程那样还需要保存,他也不知道你要干啥,所以说,他只能把你所有东西现场保护起来,等你一会等到时间片以后在都给你load回去。协程这个东西没有流行起来是因为这个API太难用了,迄今为止,我估计已经没有人会不用各种各样的库去操作协程了,它极其复杂。后来就出现了一堆这样的流派,比如,当时在golang出现之前,我们都知道HTTP服务器的一个最强因是NGINX,在nginx之后就没有敢说我能超越nginx的,至少在性能上不存在,基本上nginx是无冕之王。但是我们也知道nginx他没有线程,默认的nginx不开线程,它是会开几个进程的,而且你也可以开一个进程,而且开一个进程的性能还倍好,也基本上可以把很多东西都秒杀了。一般线上也就开四五个或者七八个nginx就已经顶天了。当然说,他的牛逼之处不在于说我因为是一个进程去处理或者多个进程去处理就牛逼,它单进程的性能也是碉堡了。或者你们想另外一个东西redis,他更奇葩,单线程单进程,但是他的网络性能也是基本上你们很少能碰到redis的网络性能不够了的。那它用的是什么魔法呢?redis其实首先没用线程,更没用协程,他用的是IO多路复用。什么意思呢?这个东西今天也就只能跟大家像扯淡一样的一种方式,让大家从一些很民科的角度去理解下这件事。古往今来就是网络的编程是个非常困难,非常复杂的一个点。为什么会很困难,很复杂呢?一个核心的原因就是说,网络由于地理,本身它可能他会隔得很远,网络读写速度和CPU内存读写速度比起来简直就是一个在地上爬,一个在天上飞。然后,问题就是说,你在天上飞的CPU可能也就是几个CPU,比如四个CPU,地上爬的这些乱七八糟的连接可能成千上万。甚至上十万,上百万。问题就是说,如果能让这些非常少的 CPU去跟这些无数个类似于慢慢吞吞的这些socket连接去完整的进行通信去把它们完整的管理起来,又高效又不占资源。就类似于什么问题呢?举个例子,这个例子很不恰当,就类似于你同时认识了五个妹子,你拿微信跟五个妹子进行聊天,你如果牛逼呢,然后就跟妹子打字也很快,其实可以五个人同时撩。我们作为一个人的经验就是我不可能是说我撩完那个妹子说我要睡觉了再去和另外一个妹子撩。我会不断的进行切换着撩。这个妹子发来信息之后呢,他可能有个消息提示,我现在正在和这个妹子打字的时候先不看提示,打完发送的时候立马切到妹子看说的什么,然后打完发送。它是这么一个模式。而不是说我就盯着这个妹子撩,但是你想想,就是说老式的这种通过线程去处理,他就是一种很蠢的模式。就是类似于我这个人我就盯着这一个妹子撩。这个妹子一分钟说一句话,或者半个小时它说我去洗澡了,你就在这等着。就是这么一个过程。所以说,后来呢,像nginx,redis这种网络服务器都借鉴这种东西去做的,就是说我一个人对你们N个人。但是这个程序写起来就会非常的复杂,因为你想,我去写一个人的逻辑是非常简单的,就类似于说我等你的响应,你给我发一句话我读完然后回复,这是一个非常非常直观的流程。写程序也是非常直观的,但是如果你是去跟N个妹子去聊,程序就会写成这么这样的一种逻辑,我先去收一下这个妹子的信息,我读一下然后我去写完回应发送,我也不等网络去发送,我也不等这个妹子回,我直接就切换过来。切换过来然后再去看下谁还有跟我说话,然后我再去把它回复一下,然后在切换回来,就变成一种,不知道你们有没有看过一个视频。就是有一个炒肉丝、炒饼的一个哥们,五个锅同时炒,这个锅翻一下,这个锅翻一下,这个锅翻一下,然后五个并行炒,就是通过这种方式进行去炒。炒五个锅的难度比炒一个锅的难度是乘以5的。这个东西设计起来呢,非常困难,为什么呢?因为你处理一个人的时候这个脑子非常简单,你就和他聊就行了。你处理5个时候你必须首先你别把妹子的名字搞混了,别突然聊着聊着把之前跟那个人的经历跟这个人的经历搞串了,这样是不行的,所以,你得有套机制,你能把你跟这个人的一些过往或者说上下文你能记住。如果你有这么一套完整的机制,聊五百万个妹子都不成问题。因为你切换的足够快,只是往内存中记个上下文而已。网络传输你并不知道他们是在海南还是在云南,还是在美国的 旧金山 或者加拿大的多伦多。这个是说不清楚的,所以说,这种东西它写代码的这个逻辑或变得异常的复杂,你去看下nginx里面有个叫event.c的文件,或者说你现在直接去看nginx代码,完全不是可能你们想象中的那种你能知道它在干什么的代码。你进去之后你就懵逼了,不知道他们程序写的是什么鬼。它是一种状态机,用行话来讲,叫有限状态机编程。这个东西很难,但是所有人都会遇到同样的问题,不只是socket编程,比如说我们遇到另外一件事,或者线上经常遇到的一件事,比如说很多公司的服务其实好多就是一个服务,无论web是php还是java写的,后头连接mysql,有一点你是特别特别怕的,数据库卡住了,数据库一旦卡住一个连接就会可能导致雪崩式的这种。因为数据库卡住了,你这个线程完全就在等她了,你也不能接收新的请求,这个时候可能很多服务器会继续开线程、开线程、开线程,开线程……开到一定地步之后,整个服务器就完蛋了。就会造成一种雪崩。

为什么要说这么一个道理呢?就是说写程序的时候你因为事实和一些现实的原因导致你很多时候需要等一些东西完成你才能继续走你下面的逻辑,然后你想吧这些东西的逻辑不去等的话就要去记录他的状态,这个东西很烦很烦,所以说就导致这种编程模式很少有人可以听得明白,我记得我当时有一次讲了三天才有那么几个人隐隐约约的听明白是怎么回事。但是写还是照样写不出来,就是写那个有限状态机。

golang的出现他用了一种很奇葩的方式解决这个问题,golang就告诉你,你就用写那种开线程的那种模式,一门走到黑的这种业务逻辑的方式去写逻辑,然后你吧这个逻辑写完之后调用一下go,吧这个逻辑go出去,他在这个语言运行层面自动去调用里面所有的东西,这样就做的非常牛逼了。就类似于你这个人变成影子分手术的一种感觉,我一下子就分身了。而且这个分身因为成本极低,开几百万个都没问题。所以说这是go的一种模式,所以说,time-server.go文件中的go handle(conn)就是这么一个go,这就是他开展了影分身术,我就直接开一个分身出来去处理这种东西,然后对于go handle(conn)这行所停留的时间仅仅就是开这个分身的时间,具体执行什么东西他根本不管,反正是那个人干去了。就是这套逻辑,所以说,你看这段代码非常简单,非常蠢,但是他跟nginx基本上可以比肩。go的牛逼之处就在这里。它让你的心智负担会大大降低。

golang的这种模式业界叫做CSP模式,C10K问题golang就是用这种方式去解决的。就是说写出一个能充分利用多核的程序需要很深的系统编程积淀。它是非常难的。

Golang因为一堆的特性,所以说他在很多地方用的都比较重。比如目前三个比较知名的项目,docker kubernetes和etcd等等还有很多。都是用golang写的,不知道有哪些同学在线上跑docker kubernetes和etcd这些东西的。

go常用工具

一键编译go build

go build github.com/humingzhe/golang/test
 

一键测试 go test(跑单测)

go test github.com/humingzhe/golang/test
 

一键下载所有更新依赖并编译go get,go get能把他依赖的东西全部的下载下来。

go get github.com/humingzhe/golang/test
 

自动文档工具godoc。他是拿来自己生成文档的,就是我得有一些go的代码,然后按照他的标准去写,然后自动能生成这些代码的文档。

godoc -http=:9090
 

这样就可以访问和golang.org一样的网站,如果不能访问golang.org的时候就可以这样访问,而且建议一般平时就这样访问速度更快,如图55所示。

图55

当然,也可以模拟在golang.org的官网上下载golang的安装包,前提是你要有梯子才行。

查看在线文档

godoc.org/github.com/golang/protobuf/proto

也可以通过godoc.org/github.com/golang/protobuf/proto这个网址去查看在线的文档。

git概念和操作

相关文章