1.2. 命令行参数

大多数的程序都是处理输入,产生输出;这也正是“计算”的定义。但是一个程序要如何获取输入呢?一些程序会生成自己的数据,但通常情况下,输入都来自于程序外部:比如文件、网络连接、其它程序的输出、用户的键盘、命令行的参数或其它类似输入源。下面几个例子会讨论其中的一些输入类型,首先是命令行参数。

os这个package提供了操作系统无关(跨平台)的,与系统交互的一些函数和相关的变量,运行时程序的命令行参数可以用一个叫os包中的Args这个变量来获取;在外部需要使用该变量时,需要用os.Args来访问。

os.Args这个变量是一个字符串(string)的slice,slice在go语言里是一个基础的数据结构,之后我们很快会提到。现在可以先把slice当一个简单的元素序列,可以用类似s[i]的下标访问形式获取其内容,并且可以用形如s[m:n]的形式来获取到一个slice的子集(译注:和python里的差不多)。其长度可以用len(s)函数来获取。和其它大多数语言差不多,go语言里的这种索引形式也采用了开区间,包括m~n的第一个元素,但不包括最后那个元素(译注:比如a = [1, 2, 3, 4, 5], a[0: 3] =[1, 2, 3],不包含最后一个元素)。这样可以简化我们的逻辑。比如s[m:n]这个slice,0 ≤ m ≤ n ≤ len(s),包含n-m个元素。

os.Args的第一个元素,即os.Args[0]是命令行执行时的命令本身;其它的元素则是执行该命令时传给这个程序的参数。前面提到的切片表达式,s[m:n]会返回第m到第n-1个元素,所以下一个例子里需要用到的os.Args[1:len(os.Args)]即是除了命令本身外的所有传入参数。如果我们省略s[m:n]里的m和n,那么默认这个表达式会填入0:len(s),所以这里我们还可以省略掉n,写os.Args[1:]。

下面是一个Unix里echo命令的实现,这个命令会在单行内打印出命令行参数。这个程序import了两个package,并且用括号把这两个package包了起来,这是分别import各个package声明的简化写法。当然了你分开来写import也没有什么问题,只是一般为了方便我们都会像下面这样来导入多个package。我们自己写的导入顺序并不重要,因为gofmt工具会帮助我们按照字母顺序来排列好这些导入包名。(本书中如果一个例子有多种版本时,我们会用编号标记出来)

gopl.io/ch1/echo1
// Echo1 prints its command-line arguments.
package main
import (
    "fmt"
    "os"
)
func main() {
     var s, sep string
     for i := 1; i < len(os.Args); i++ {
         s += sep + os.Args[i]
         sep = " "
     }
     fmt.Println(s)
}

Go里的注释是以//来表示。//后的内容一直到行末都是这条注释的一部分,并且这些注释会被编译器忽略。

按照惯例,我们会在每一个package前面放上这个package的详尽的注释对其进行说明;对于一个main package来说,一般这段评论会包含几句话来说明这个项目/程序整体是做什么用的。

var关键字用来做变量声明。这里声明了s和sep两个string变量。变量可以在声明期间直接进行初始化。如果没有显式地初始化的话,Go会隐式地给这些未初始化的变量赋予对应其类型的零值,比如数值类型就是0,字符串类型就是“”空字符串。在这个例子里的s和sep被隐式地赋值为了空字符串。在第2章中我们会更详细地讲解变量和声明。

对于数字类型,Go语言提供了常规的数值计算和逻辑运算符。而对于string类型,+号表示字符串的连接(译注:和C++或者js是一样的)。所以下面这个表达式:

sep + os.Args[i]

表示将sep字符串和os.Args[i]字符串进行连接。我们在程序里用的另外一个表达式:

s += sep + os.Args[i]

会将sep与os.Args[i]连接,然后再将得到的结果与s进行连接,这种方式和下面的表达是等价的:

s = s + sep + os.Args[i]

运算符+=是一个赋值运算符(assignment operator),每一种数值和逻辑运算符,例如*或者+都有其对应的赋值运算符。

echo程序可以每循环一次输出一个参数,不过我们这里的版本是不断地将其结果连接到一个字符串的末尾。s这个字符串在声明的时候是一个空字符串,而之后循环每次都会被在末尾添加一段字符串;第一次迭代之后,一个空格会被插入到字符串末尾,所以每插入一个新值,都会和前一个中间有一个空格隔开。这是一种非线性的操作,当我们的参数数量变得庞大的时候(当然不是说这里的echo,一般echo也不会有太多参数)其运行开销也会变得庞大。下面我们会介绍一系列的echo改进版,来应对这里说到的运行效率低下。

在for循环中,我们用到了i来做下标索引,可以看到我们用了:=符号来给i进行初始化和赋值,这是var xxx=yyy的一种简写形式,Go会根据等号右边的值的类型自动判断左边的值类型,下一章会对这一点进行详细说明。

自增表达式i++会为i加上1;这个i += 1以及i = i + 1都是等价的。对应的还有i--是给i减去1。这些在go语言里是语句,而不像C系的其它语言里是表达式。所以在Go语言里j = i++是非法的,而且++和--都只能放在变量名后面,因此--i也是非法的。

在Go语言里只有for循环一种循环。当然了为了满足需求,Go的for循环有很多种形式,下面是其中的一种:

for initialization; condition; post {
    // zero or more statements
}

这里需要注意,for循环的两边是不需要像其它语言一样写括号的。并且左大括号需要和for语句在同一行。

initialization部分是可选的,如果你写了这部分的话,在for循环之前这部分的逻辑会被执行。需要注意的是这部分必须是一个简单的语句,也就是说是一个简短的变量声明,一个赋值语句,或是一个函数调用。condition部分必须是一个结果为boolean值的表达式,在每次循环之前,语言都会检查当前是否满足这个条件,如果不满足的话便会结束循环;post部分的语句则是在每次循环结束之后被执行,之后conditon部分会在下一次执行前再被执行,依此往复。当condition条件里的判断结果变为false之后,循环即结束。

上面提到是for循环里的三个部分都是可以被省略的,如果你把initialization和post部分都省略的话,那么连中间隔离他们的分号也是可以被省略的,比如下面这种for循环,就和传统的while循环是一样的:

// a traditional "while" loop
for condition {
    // ...
}

当然了,如果你连唯一的条件都省了,那么for循环就会变成一个无限循环,像下面这样:

// a traditional infinite loop
for {
    // ...
}

在无限循环中,你还是可以靠break或者return来终止掉循环。

如果你的遍历对象是string或者slice里的值的话,还有另外一种循环的写法,我们来看看另一个版本的echo:

gopl.io/ch1/echo2
// Echo2 prints its command-line arguments.
package main

import (
    "fmt"
)

func main() {
    s, sep := "", ""
    for _, arg := range os.Args[1:] {
        s += sep + arg
        sep = " "
    }
    fmt.Println(s)
}

每一次循环迭代,range都会返回一对结果;当前迭代的下标以及在该下标处的元素的值。在这个例子里,我们不需要这个下标,但是因为range的处理要求我们必须要同时处理下标和值。我们可以在这里声明一个接收index的临时变量来解决这个问题,但是go语言又不允许只声明而在后续代码里不使用这个变量,如果你这样做了编译器会返回一个编译错误。

在Go语言中,应对这种情况的解决方法是用空白标识符,对,就是上面那个下划线。空白标识符可以在任何你接收自己不需要处理的值时使用。在这里,我们用他来忽略掉range返回的那个没用的下标值。大多数的Go程序员都会像上面这样来写类似的os.Args遍历,可以避免错误的下标引用。(这里可能有翻译错,附上原文) Most Go programmers would likely use range and to write the echo program as above, since the indexing over os.Args is implicit, not explicit, and thus easier to get right.

上面这个版本将s和sep的声明和初始化都放到了一起,但是我们可以等价地将声明和赋值分开来写,下面这些写法都是等价的

s := ""
var s string
var s = ""
var s string = ""

那么这些等价的形式应该怎么做选择呢?这里提供一些建议:第一种形式,最好只用在一个函数内部,而package级别的变量,请不要使用这样的声明方式。第二种形式依赖于string类型的内部初始化机制,被初始化为空字符串。第三种形式使用得很少,除非同时声明多个变量。第四种形式会显式地标明变量的类型,在多变量同时声明时可以用到。实践中你应该只使用上面的前两种形式,显式地指定变量的类型,让编译器自己去初始化其值,或者直接用隐式初始化,表明初始值怎么样并不重要。

像上面提到的,每次循环中字符串s都会得到一个新内容。+=语句会分配一个新的字符串,并将老字符串连接起来的值赋予给它。而目标字符串的老字面值在得到新值以后就失去了用处,这些临时值会被go的垃圾收集器干掉。

如果不断连接的数据量很大,那么上面这种操作就是成本非常高的操作。更简单并且有效的一种方式是使用字符串的Join函数,像下面这样:

gopl.io/ch1/echo3
func main() {
    fmt.Println(strings.Join(os.Args[1:], " "))
}

最后,如果我们对输出的格式也不是很关心,只是想简单地输出值得的话,还可以像下面这么写,Println函数会为我们自动格式化输出。

fmt.Println(os.Args[1:])

这个输出结果和前面的string.Join得到的结果很相似,只是被自动地放到了一个括号里,对slice调用Println函数都会被打印成这样形式的结果。

下面是几道练习题:

Exercise 1.1:修改echo程序,使其能够打印os.Args[0]。
Exercise 1.2:修改echo程序,使其打印value和index,每个value和index显示一行。
Exercise 1.3:上手实践前面提到的strings.Join和直接Println,并观察输出结果的区别。