|
| 1 | +我们从经典的Hello Word程序开始,1978年《C语言编程》出版的时候也是这么干的。C应该说是对Go语言产生直接影响最大的语言之一,从Hello World这个例子我们就能看出一些端倪 |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +Go是一个编译语言。Go工具链会把源代码以及相关依赖包转换成为计算机底层原生机器语言的指令。这些工具链只通过一个命令来管理:go。go命令有一系列的子命令,对应各个工具集操作。最简单的子命令就是run,该命令可以编译一个或多个命名为.go的源代码文件,链接依赖库,然后运行编译结果(但是不会留下编译好的可执行文件)。我们在本书中用$符号表示命令提示符。 |
| 6 | + |
| 7 | +$ go run hellowolrd.go |
| 8 | + |
| 9 | +毫无悬念,屏幕上将会得到一下输出: |
| 10 | + |
| 11 | +Hello,世界 |
| 12 | + |
| 13 | +Go原生自带Unicode的处理,所以可以处理世界上各种语言的文本。 |
| 14 | + |
| 15 | +如果程序只实现性的运行一次无法满足需求,那么你应该会想要保存编译出的结果已被将来调用。那么这时就应该使用go build命令: |
| 16 | + |
| 17 | +$ go build helloworld.go |
| 18 | + |
| 19 | +然后一个名为helloworld的可执行的二进制文件就编译出来了,下次直接运行可执行文件即可,不用在进行重复的编译过程。 |
| 20 | + |
| 21 | +$ .\/helloworld |
| 22 | + |
| 23 | +Hello, 世界 |
| 24 | + |
| 25 | +为了方便使用,本书的代码都标注好了名字,你可以在gopl.io\/ch1\/helloworld下获得刚才的源码。 |
| 26 | + |
| 27 | +如果你运行go get gopl.io\/ch1\/helloworld, 该命令就会从网络上对应地址获取源码,存放到本地的相应目录中。在2.6节和10.7节中会有更详细的描述。 |
| 28 | + |
| 29 | +现在我们来仔细看下这个程序本身。Go代码以包的形式来管理,包的概念和其他一些语言里库的概念或是模块的概念相似。一个包由一个或多个.go源文件组成,这些源文件放在同一个目录里,里面定义了这个包的功能实现。每一个源文件开头都是一个包的声明,在这个例子里导入声明是pacakage main, 说明了该文件所属的包叫做main。后面跟着该文件需要导入的其他包的名字,然后是代码文件本身实现的程序的声明。 |
| 30 | + |
| 31 | +Go标准库有超过100个包,涵盖各种常见任务的实现, 包括输入和输出,排序,以及文本处理。比如说fmt包里面有实现格式化打印输出的函数和扫描输入的函数。Println是fmt中的一个基本输出函数。它打印出一个或多个值,用空格隔开,在行尾会加一个换行符,这样每一个值打印出来后都会新起一行。 |
| 32 | + |
| 33 | +Package main是一个特殊的包,它定义了一个独立的可执行程序,而不是一个library。在package main 中同样有一个特殊的函数,main函数,这是一个可执行程序的入口。main所做的事情就是这个程序做的事情。当然,main通常是靠调用其他包里面的其它函数来完成这些事情的,比如调用fmt.Println. |
| 34 | + |
| 35 | +我们必须告诉编译器当前源文件到底需要哪些package的依赖,这正是import 声明的使命,import声明跟在package 声明之后。hello world程序只是用了外部的一个包中的一个函数,但是大多数程序会依赖更多的package。 |
| 36 | + |
| 37 | +你必须精确的进行包引用。如果少了一个包,或是多饮用了一些没有用到的包,编译的时候都会出错。这个严格要求避免了程序演进的过程中越来越多的引用一些不必要的包而使得程序整体变得不必要的臃肿。 |
| 38 | + |
| 39 | +import声明必须跟在package声明的后面。在那之后会是程序的其他组成部分:函数声明, 变量, 常量, 还有类型定义(分别有各自的关键字定义: func, var, const, 还有type\);对于大多数组成部分而言,声明的顺序并不重要。示例程序很简短,只定义了一个函数,而这个函数也正好只调用了一个别的函数。为了节省空间,我们在演示时有时会隐藏package和import声明,但是源文件中肯定是有这些声明的,否则无法编译。 |
| 40 | + |
| 41 | +一个函数声明包括函数关键字func,函数名,参数列表(此例中main函数的函数列表是空列表),一个结果列表(这里也是空列表),还有函数体,也就是定义函数行为的语句们。函数体用花括号包起来。我们会在第五章仔细研究函数相关问题。 |
| 42 | + |
| 43 | +Go并不要求每一个语句后面加一个分号,除非一行写多个语句,那么前几个语句之间必须用分号隔开。事实上在编译之前带有特殊符号的换行符会被转化尾分号,所以换行符的位置在Go代码中是有讲究的。比如,开始的花括号{必须和函数声明在同一行,不能单独起一行,而在表达式x+y中,+号后面可以有换行,但是+号前面不可以(否则会把+y认成一个新的变量)。 |
| 44 | + |
| 45 | +Go在代码格式上的要求非常严格。gofmt工具会把代码转换成标准go代码格式的写法,go的fmt 子命令会给指定包中的所有源码文件进行gofmt格式化,默认值是给当前目录格式化。本书所有Go源文件都已经公国gofmt进行了格式化,你在编写自己的代码时也应该养成使用gofmt的习惯。强制使用标准格式减少了很多关于细节的无意义讨论,并且更重要的是使得自动化的代码转换变为了可能,而这在允许随意的编码格式的语言中是不可实现的。 |
| 46 | + |
| 47 | +许多文本编辑器可以配置为每次保存的时候自动运行gofmt, 这样你的代码就会永远保持标准格式了。还有一个相关的工具叫做goimports,会自动的管理引用的package,不足缺失的包,删掉多余的包。它并没有集成在标准发布版当中,不过你可以通过以下命令来获得这个命令工具: |
| 48 | + |
| 49 | +$ go get golang.org\/x\/tools\/cmd\/goimports |
| 50 | + |
| 51 | +对大多数用户来说,通用的操作都离不开go 工具,比如下载或是编译package,测试包,查看包的文档等等。我们会在10.7节详细看看go工具的使用。 |
| 52 | + |
| 53 | +### 1.2. 命令行参数 |
| 54 | + |
| 55 | +大多数程序本质上都是处理某种输入,然后产出某种输出。这可以看成是计算的一种定义。那么问题来了,程序是如何获得需要处理的输入数据的呢?有些程序自己产生输入数据,但是更常见的是通过一个外部的源来获取数据: 比如通过文件,通过网络连接,通过其他程序的输出,用户的键盘输入,以及命令行参数等等。接下俩的几个例子会讨论这几种不同的方式,我们先从命令行参数说起。 |
| 56 | + |
| 57 | +os包提供了和操作系统交互的函数,还可以直接获得某些和系统相关的值。这些函数和变量都是非平台相关的,也就是说在使用的时候可以不考虑所在操作系统,用法都是相同的。命令行参数可以通过os包中一个名为Args的变量获得,因此在os包之外的任何地方我们都用os.Args来访问该变量。 |
| 58 | + |
| 59 | +os.Arg是一个string组成的slice。slice是Go的一种基本类型。我们很快会详细谈到他。现在先吧slice看成是一个大小动态变化的数组s, 每一个数组元素可以通过下角标s\[i\]访问,也可以通过子序列s\[m:n\]来获取其中的一段生成一个新的slice。元素的个数通过len\(s\)获得。和大多数其他编程语言一样,Go的index也是用左闭右开区间, 这样可以简化逻辑。比如,slice s\[m:n\]包含n-m个元素。从s\[m\]到s\[n-1\] |
| 60 | + |
| 61 | +os.Args的第一个元素是os.Args\[0\], 就是命令名本身。os.Args剩下的其他元素就是开始执行是传入的参数。表达式s\[m:n\]表示从第m个元素到第n-1个元素组成的一个新slice。如果m或是n省略掉的话,表达式就表示从0开始或是直到len\(s\)的slice。所以,我们可以把想要的slice简记为os.Args\[1:\]。 |
| 62 | + |
| 63 | +这里我们来看一个Unix系统里 echo命令的简单实现。该命令可以把其参数用一行给打印出来。这里导入了两个包,注意不是用独立的import语句依次导入,而是用花括号包起来用一个import语句导入的。这两种写法都是对的,不过一般都用花括号的方式更方便一些。导入的顺序无关紧要,gofmt会把这些包名按照首字母顺序重排的。(对于有多个版本的示例程序,我们会用后面的数字来方读者区分) |
| 64 | + |
| 65 | +TODO1 p5 |
| 66 | + |
| 67 | +注释以双斜杠\/\/开头。\/\/后面到行尾的所有文本都会被编译器忽略掉,只是供程序员注释使用。习惯上我们会在包声明语句的前面一句用注释说明包的用途。对于main package,这句注释会是描述整个程序功能的一两句话。 |
| 68 | + |
| 69 | +关键字var声明了连个变量s还有sep,都是string类型。变量可以在声明的时候就初始化。如果没有进行显式的初始化,那该变量就会被隐式的初始化为对应类型的零值。比如数字类型就会被初始化为0, string类型就会被初始化为空字符串""。因此在本例中,s和sep都被隐式的初始化为空字符串。我们在第二章中会详细讨论变量声明。 |
| 70 | + |
| 71 | +对于数字类型,Go提供了通用的数学运算符和逻辑运算符。不过+号运算符用于string的时候表示连接操作。 |
| 72 | + |
| 73 | +所以sep+os.Args\[i\]这个表达式表示把sep和os.Args\[i\]连接起来,程序中的语句 |
| 74 | + |
| 75 | +s+= sep+os.Args\[i\] |
| 76 | + |
| 77 | +是一个赋值语句,把运算前的s,sep,还有os.Args\[i\]连接起来,然后赋值给s。该语句等价为 |
| 78 | + |
| 79 | +s = s+sep+os.Args\[i\] |
| 80 | + |
| 81 | ++=运算符是一个赋值运算符。每一个像+或是\*这样的数学运算符和逻辑运算符都有一个类似这样的赋值运算符。 |
| 82 | + |
| 83 | +echo程序其实可以在每一次循环中打印出循环对应的那一部分内容。不过这个版本选用的方式是 每次把新的文本添加到string的末尾,把所有结果拼到一个string中然后一次打印出来。string s一开始是空字符串“”, 然后每一次循环都会在末尾加入一些文本,并且每次迭代都插入一个空格,这样最后循环结束时参数之间会有空格分隔。这是以一个二元的过程,当参数比较多的时候会有性能问题。不过对于echo来说这种情况不太会发生。我们会在本章演示其他版本的echo,解决性能低下的问题。 |
| 84 | + |
| 85 | +循环的索引值i实在for循环的第一部分声明的。:=符号表明一个简短的变量声明,所在语句声明一个或多个变量,然后初始化它们,下一章会详细的讨论变量声明。 |
| 86 | + |
| 87 | +自增语句i++每次给i上加1, 等价于i+=1, 也等价于i=i+1;对应的还有一个自减运算i--,每次给i减1。自增和自减都是语句,不是表达式。这一点和C语言家族不一样,所以j=i++这样的写法是不允许的,并且也只能有后缀形式,所以--i这种写法也是非法的。 |
| 88 | + |
| 89 | +for循环是Go语言唯一支持的循环语句。有好几种形式,其中的一种如下: |
| 90 | + |
| 91 | +for initialization; condition; post{ |
| 92 | + |
| 93 | +\/\/ zero or more statements |
| 94 | + |
| 95 | +} |
| 96 | + |
| 97 | +for循环的三个部分都不需要括号,但是循环体必须用花括号括起来,而且开头的花括号必须和post语句在同一行。 |
| 98 | + |
| 99 | +可选的initialization语句在循环开始之前被执行,如果有这句的话必须是一个简单语句,可以使一个简短的变量声明,一个自增或是赋值语句,或是一个函数调用。condition是一个boolean表达式,每次循环迭代之初都会判断一下condition是否为true, 如果是true则循环迭代一次。post语句则是在每次循环迭代完成之后被调用,然后开始下一轮,condition又开始判断。当condition编程false的时候,循环结束。 |
| 100 | + |
| 101 | +这三部分都可以被省略。如果initialization和post都没有,那么分号也可以省略: |
| 102 | + |
| 103 | +\/\/传统的while也就是这个玩意儿 |
| 104 | + |
| 105 | +for condition{ |
| 106 | + |
| 107 | +\/\/... |
| 108 | + |
| 109 | +} |
| 110 | + |
| 111 | +如果condition被省略掉了,不管另两部分怎么样,都会成为一个死循环,比如 |
| 112 | + |
| 113 | +\/\/相当于传统的死循环 |
| 114 | + |
| 115 | +for{ |
| 116 | + |
| 117 | +\/\/... |
| 118 | + |
| 119 | +} |
| 120 | + |
| 121 | +这个循环本身是无限循环下去的,不过这种形式的循环语句可以用其他方式结束,比如通过break或是return语句; |
| 122 | + |
| 123 | +for循环的另一种形式是在一个区间上循环执行,比如string的一段或是slice的子slice。为了说明这一点我们来看看第二版echo的代码 |
| 124 | + |
| 125 | +\/\/TODO 2 page 25 |
| 126 | + |
| 127 | +在每一个循环迭代中,range产生一组值:index,还有slice对应index位置上的值。在这个例子种,我们不需要索引值,不过range循环的语法要求我们必须成对处理index和对应value。一个自然地想法是把index赋给一个临时变量temp然后忽略这个值,不过Go的优雅不允许存在没使用过的局部变量,这会导致一个编译错误。 |
| 128 | + |
| 129 | +解决方案是使用一个下划线表示空标识符。空标识符可以用在任何一个语法要求变量名但是逻辑上并不使用该变量的场合。比如在你只需要元素值的地方来处理语法上龟毛强制获得的index。大多数Go码农都会像上面那样使用range和\_来写echo程序,因为os.Args的索引值其实是隐式存在的,并没有显式的声明出来,因此这么写更不容易出错。 |
| 130 | + |
| 131 | +这一版的程序使用了变量的简短声明来声明和初始化s还有sep, 不过我们也可以分开声明他们。这里有几种完全等价的string声明方式: |
| 132 | + |
| 133 | +s := "" |
| 134 | + |
| 135 | +var s string |
| 136 | + |
| 137 | +var s = "" |
| 138 | + |
| 139 | +var s string = "" |
| 140 | + |
| 141 | +那么应该怎么选择呢?第一种写法是简明变量声明法,最为简洁,不过只能在函数内部使用,不能用于声明package级别的变量。第二种写法依赖了隐式的默认零值初始化,把string初始化为"".第三种写法很少用到,除非是在声明多个变量的时候。第四种写法显式的声明了变量类型,这在初始化值为同样类型时显得有点多余,不过在二者类型不同的时候就显得很必要了。在实际工作中,通常你应该使用第一种和第二种写法,显式初始化用于表明该变量的初始值很重要,而隐式初始化表明该变量的初始值并不重要。 |
| 142 | + |
| 143 | +正如上面所述,每一次循环迭代,string s都会得到全新的内容。+=语句通过拼接原来的string,一个空格字符,还有下一个参数文本,得到一个全新的string, 然后赋值给s。原来s的内容不再被使用,所以在一定的时候会被GC回收掉。 |
| 144 | + |
| 145 | +如果涉及的数据比较大,GC的操作就会开销比较大。更简单且效率更高的方案是使用strings包的Join函数,一句就搞定: |
| 146 | + |
| 147 | +gopl.io\/ch1\/echo3 |
| 148 | + |
| 149 | +func main\(\){ |
| 150 | + |
| 151 | +fmt.Println\(strings.Join\(os.Args\[1:\]," "\) |
| 152 | + |
| 153 | +} |
| 154 | + |
| 155 | +最后,如果我们不太关心显示的格式,而只是想看一下值,比如是为了调试,那么我们可以让Println来为我们格式化结果数据: |
| 156 | + |
| 157 | +fmt.Println\(os.Args\[1:\]\) |
| 158 | + |
| 159 | +这句语句的输出和我们从strings.Join得到的差不多,只不过多了一个中括号。所有的slice都可以这样打印。 |
| 160 | + |
| 161 | + |
| 162 | + |
0 commit comments