Golang 第一章 2.命令行参数
大多数程序处理一些输入以产生一些输出;这几乎就是计算的定义。但是程序如何获取要操作的输入数据?有些程序会生成自己的数据,但更常见的是,输入来自外部来源:文件、网络连接、另一个程序的输出、键盘上的用户、命令行参数等。接下来的几个示例将从命令行参数开始讨论其中一些替代方案。
os 包提供了以平台无关的方式处理操作系统的函数和其他值。命令行参数在 os 包中名为 Args 的变量中可供程序使用;因此,在 os 包之外的任何地方,它的名称都是 os.Args。
变量 os.Args 是字符串的切片。切片是 Go 中的基本概念,我们很快会更多地讨论它们。现在,将切片视为动态大小的数组元素序列 s,其中各个元素可以通过 s[i] 访问,而连续的子序列可以通过 s[m:n] 访问。元素的数量由 len(s) 给出。与大多数其他编程语言一样,Go 中的所有索引都使用半开区间,包括第一个索引但不包括最后一个索引,因为它简化了逻辑。例如,切片 s[m:n],其中 0 ≤ m ≤ n ≤ len(s),包含 n-m 个元素。
os.Args 的第一个元素 os.Args[0] 是命令本身的名称;其他元素是程序开始执行时提供给它的参数。形式为 s[m:n] 的切片表达式产生的切片引用元素 m 到 n-1,因此下一个示例所需的元素是切片 os.Args[1:len(os.Args)] 中的元素。如果省略 m 或 n,则分别默认为 0 或 len(s),因此我们可以将所需的切片缩写为 os.Args[1:]。
这是 Unix echo 命令的实现,它在一行上打印其命令行参数。它导入两个包,这两个包以括号列表的形式给出,而不是单独的导入声明。两种形式都是合法的,但通常使用列表形式。导入的顺序无关紧要;gofmt 工具将包名称按字母顺序排序。(当一个示例有多个版本时,我们通常会对它们进行编号,以便您确定我们在讨论哪一个。)
gopl.io/ch1/echo1
// Echo1 打印其命令行参数。
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)
}
注释以 // 开头。从 // 到行尾的所有文本都是程序员的注释,编译器会忽略它们。按照惯例,我们会在每个包声明前的注释中描述每个包;对于主包,此注释是一个或多个完整的句子,用于描述整个程序。
var 声明声明了两个字符串类型的变量 s 和 sep。变量可以在声明过程中初始化。如果未显式初始化,则隐式初始化为其类型的零值,对于数字类型为 0,对于字符串为空字符串 ""。因此,在此示例中,声明隐式将 s 和 sep 初始化为空字符串。我们将在第 2 章中详细介绍变量和声明。
对于数字,Go 提供了常用的算术和逻辑运算符。但是,当应用于字符串时,+ 运算符会连接值,因此表达式
sep + os.Args[i]
表示字符串 sep 和 os.Args[i] 的连接。我们在程序中使用的语句,
s += sep + os.Args[i]
是一个赋值语句,将 s 的旧值与 sep 和 os.Args[i] 连接起来,并将其赋值回 s;它相当于
s = s + sep + os.Args[i]
运算符 += 是赋值运算符。每个算术运算符和逻辑运算符(例如 + 或 *)都有相应的赋值运算符。
echo 程序本来可以一次打印一段输出,但这个版本通过反复将新文本附加到末尾来构建一个字符串。字符串 s 的初始值为空,即值为“”,每次循环都会向其添加一些文本;在第一次迭代之后,还会插入一个空格,这样当循环结束时,每个参数之间都会有一个空格。这是一个二次过程,如果参数数量很大,则成本可能很高,但对于 echo 来说,这种情况不太可能发生。我们将在本章和下一章中展示一些改进的 echo 版本,以解决任何实际的低效率问题。
循环索引变量 i 在 for 循环的第一部分中声明。 := 符号是短变量声明的一部分,该语句声明一个或多个变量并根据初始化值赋予它们适当的类型;下一章将对此进行更多介绍。
增量语句 i++ 将 i 加 1;它相当于 i +=1,而后者又相当于 i=i+ 1。还有一个相应的减量语句 i--,它将 i 减 1。这些是语句,而不是表达式,不像 C 家族中的大多数语言那样,所以 j= i++ 是非法的,而且它们只是后缀,所以 --i 也是非法的。
for 循环是 Go 中唯一的循环语句。它有多种形式,其中一种如下所示:
for initialization; condition; post {
// zero or more statements
}
for 循环的三个部分永远不能用圆括号括起来。但是,括号是必须的,并且左括号必须与后置语句位于同一行。
可选的初始化语句在循环开始前执行。如果存在,它必须是一个简单的语句,即一个简短的变量声明、一个增量或赋值语句,或者一个函数调用。条件是一个布尔表达式,在循环的每次迭代开始时进行判断;如果判断结果为真,则执行由循环控制的语句。后置语句在循环主体之后执行,然后再次判断条件。当条件变为假时,循环结束。
这些部分中的任何一个都可以省略。如果没有初始化和后置,分号也可以省略:
// a traditional "while" loop for
condition {
// ...
}
如果以任何一种形式完全省略条件,例如
// a traditional infinite loop for {
// ...
}
循环是无限的,但是这种形式的循环可以通过其他方式终止,例如 break 或 return 语句。
for 循环的另一种形式是迭代某个数据类型(如字符串或切片)中的一系列值。为了说明这一点,下面是 echo 的第二个版本:
gopl.io/ch1/echo2
// Echo2 打印其命令行参数。
package main
import (
"fmt"
"os"
)
func main() {
s, sep := "", ""
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
fmt.Println(s)
}
在循环的每次迭代中,range 都会产生一对值:索引和该索引处元素的值。在此示例中,我们不需要索引,但 range 循环的语法要求,如果我们处理元素,也必须处理索引。一种想法是将索引分配给明显是临时的变量(如 temp)并忽略其值,但 Go 不允许未使用的局部变量,因此这会导致编译错误。
解决方案是使用空白标识符,其名称为 _(即下划线)。当语法需要变量名但程序逻辑不需要时,可以使用空白标识符,例如,当我们只需要元素值时,丢弃不需要的循环索引。大多数 Go 程序员可能会使用 range 和 _ 来编写上述 echo 程序,因为对 os.Args 的索引是隐式的,而不是显式的,因此更容易正确。
此版本的程序使用短变量声明来声明和初始化 s 和 sep,但我们也可以分别声明这两个变量。声明字符串变量有多种方法;这些方法都是等效的:
s := ""
var s string
var s = ""
var s string = ""
为什么你应该选择第一种形式而不是另一种形式?第一种形式,即简短的变量声明,是最紧凑的,但它只能在函数中使用,不能用于包级变量。第二种形式依赖于字符串的默认初始化为零值,即“”。第三种形式很少使用,除非声明多个变量。第四种形式明确说明了变量的类型,当变量的类型与初始值相同时,这是多余的,但在它们不是同一类型时,这是必要的。在实践中,你通常应该使用前两种形式中的一种,使用显式初始化表示初始值很重要,使用隐式初始化表示初始值无关紧要。
如上所述,每次循环,字符串 s 都会获得全新的内容。 += 语句通过连接旧字符串、空格字符和下一个参数来创建一个新字符串,然后将新字符串分配给 s。 s 的旧内容不再使用,因此它们将在适当的时候被垃圾回收。
如果涉及的数据量很大,这可能会很浪费资源。一个更简单、更有效的解决方案是使用 strings 包中的 Join 函数:
gopl.io/ch1/echo3
package main
import (
"fmt"
"os"
"strings"
)
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
最后,如果我们不关心格式而只想查看值,也许是为了调试,我们可以让 Println 为我们格式化结果:
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println(os.Args[1:])
}
此语句的输出类似于我们从 strings.Join 获得的输出,但带有括号。任何切片都可以通过这种方式打印。
评论