1.2. 命令行参数

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

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

os.Args这个变量是一个字符串(string)的slice(译注:slice和Python语言中的切片类似,是一个简版的动态数组),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也没有什么问题,只是这么写更加方便。这里的导入顺序并不重要,因为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 = 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循环之前这部分的逻辑会被执行。initalization部分必须是一个简单的语句,具体可以是一个简短的变量声明,一个赋值语句,或是一个函数调用。condition部分必须是一个结果为boolean值的表达式,在每次循环之前,语言都会检查当前是否满足这个条件,若不满足的话便会结束循环;post部分的语句则是在每次循环迭代结束之后被执行,之后conditon部分会在下一次执行前再次进行判断,依此往复。当condition条件里的判断结果变为false之后,循环即结束。

上面提到是for循环里的三个部分(initialization/condition/post)都是可以被省略的,如果你把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函数要求我们必须同时处理下标和元素两个返回值。这种时候可以在声明一个接收下标的临时变量来解决这个问题,但Go语言又不允许只声明变量而在后续代码里不使用,如果你这样做了编译器会返回一个编译错误。

Go语言中这种情况的解决方法是用空白标识符,对,就是代码里那个下划线_。空白标识符可以在任何你需要接收自己不想处理的值时使用。这里使用它来忽略掉range返回的那个没什么用的下标值。大多数的Go程序员都会像上面这样来写类似的os.Args遍历,由于遍历os.Args的下标索引是隐式自动生成的,这里也并不需要关心。

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

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

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

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

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

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

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

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

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

练习 1.1: 修改echo程序,使其能够打印os.Args[0]。

练习 1.2: 修改echo程序,使其打印value和index,每个value和index显示一行。

练习 1.3: 上手实践前面提到的strings.Join和直接Println,并观察输出结果的区别。