#4.2 Go 程序的基本结构和要素 ( ***译者注:由于 Go 语言版本更替,本节中的相关内容经原作者同意将被直接替换而不作另外说明*** ) Example 4.1 [hello_world.go](examples/chapter_4/hello_world.go) package main import "fmt" func main() { fmt.Println("hello, world") } ##4.2.1 包的概念、导入与可见性 包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。 如同其它一些编程语言中的类库或命名空间的概念,每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 `.go` 为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。 你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:`package main`。`package main`表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 `main` 的包。 一个应用程序可以包含不同的包,而且即使你只使用 main 包也不必把所有的代码都写在一个巨大的文件里:你可以用一些较小的文件,并且在每个文件非注释的第一行都使用 `package main` 来指明这些文件都属于 main 包。如果你打算编译包名不是为 main 的源文件,如 `pack1`,编译后产生的对象文件将会是 `pack1.a` 而不是可执行程序。另外要注意的是,所有的包名都应该使用小写字母。 **标准库** 在 Go 的安装文件里包含了一些可以直接使用的包,即标准库。在 Windows 下,标准库的位置在 Go 根目录下的子目录 `pkg\windows_386` 中;在 Linux 下,标准库在 Go 根目录下的子目录 `pkg\linux_amd64` 中(如果是安装的是 32 位,则在 `linux_386` 目录中)。一般情况下,标准包会存放在 `$GOROOT/pkg/$GOOS_$GOARCH/` 目录下。 Go 的标准库包含了大量的包(如:fmt,os),但是你也可以创建自己的包(第 8 章)。 如果想要构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序。 属于同一个包的源文件必须全部被一起编译,一个包既是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。 *如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。* Go 中的包模型采用了显式依赖关系的机制来达到快速编译的目的,编译器会从后缀名为 `.o` 的对象文件(需要且只需要这个文件)中提取传递依赖类型的信息。 如果 `A.go` 依赖 `B.go`,而 `B.go` 又依赖 `C.go`: - 编译 `C.go`, `B.go`, 然后是 `A.go`. - 为了编译 `A.go`, 编译器读取的是 `B.o` 而不是 `C.o`. 这种机制对于编译大型的项目时可以显著地提升编译速度。 *每一段代码只会被编译一次* 一个 Go 程序是通过 `import` 关键字将一组包链接在一起。 `import “fmt”` 告诉 Go 编译器这个程序需要使用 `fmt` 包(的函数,或其他元素),`fmt` 包实现了格式化 IO(输入/输出)的函数。包名被封闭在半角双引号 `""` 中。如果你打算从已编译的包中导入并加载公开声明的方法,不需要插入已编译包的源代码。 如果需要多个包,它们可以被分别导入: import “fmt” import “os” 或: import “fmt”; import “os” 但是还有更短且更优雅的方法(被称为因式分解关键字,该方法同样适用于 const、var 和 type 的声明或定义): import ( "fmt" "os" ) (它甚至还可以更短:`import ("fmt", "os")` 但使用 gofmt 后将会被强制换行) 当你导入多个包时,导入的顺序会按照字母排序。 如果包名不是以 `.` 或 `/` 开头,如 "fmt" 或者 "container/list" ,则 Go 会在全局文件进行查找;如果包名以 `./` 开头,则 Go 会在相对目录中查找;如果包名以 `/` 开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找。 导入包即等同于包含了这个包的所有的代码对象。 除了符号 `_`,包中所有代码对象的标识符必须是唯一的,以避免名称冲突。但是相同的标识符可以在不同的包中使用,因为可以使用包名来区分它们。 包通过下面这个被编译器强制执行的规则来决定是否将自身的代码对象暴露给外部文件: **可见性规则** 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。 (大写字母可以使用任何 Unicode 编码的字符,比如希腊文,不仅仅是 ASCII 码中的大写字母)。 因此,在导入一个外部包后,能够且只能够访问该包中导出的对象。 假设在包 pack1 中我们有一个变量或函数叫做 Thing(以 T 开头,所以它能够被导出),那么在当前包中导入 pack1 包,Thing 就可以像面向对象语言那样使用点标记来调用:`pack1.Thing`(pack1 在这里是不可以省略的) 因此包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):两个包中的同名变量的区别在于他们的包名,例如 `pack1.Thing` 和 `pack2.Thing`。 你可以通过使用包的别名来解决包名之间的名称冲突,或者说根据你的个人喜好对包名进行重新设置,如:`import fm "fmt"`。下面的代码展示了如何使用包的别名: Example 4.2 [alias.go](examples/chapter_4/alias.go) package main import fm "fmt" // alias3 func main() { fm.Println("hello, world") } **注意事项** 如果你导入了一个包却没有使用它,则会在构建程序时引发错误,如 `imported and not used: os`,这正是遵循了 Go 的格言:“没有不必要的代码!“ **包的分级声明和初始化** 你可以在使用 `import` 导入包之后定义或声明 0 个或多个常量(const)、变量(var)和类型(type),这些对象的作用域都是全局的(在本包范围内),所以可以被本包中所有的函数调用(如 [gotemplate.go](examples/chapter_4/gotemplate.go) 源文件中的 c 和 v),然后声明一个或多个函数(func)。 ##4.2.2 函数 这是定义一个函数最简单的格式:`func functionName()` 你可以在括号`()`中写入 0 个或多个函数的参数(使用`,`分离),每个参数的名称后面必须紧跟着该参数的类型。 main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数( ***译者注:如果有 init() 函数则会先执行该函数*** )。如果你的 main 包的源代码没有包含 main 函数,则会引发构建错误 `undefined: main.main`。main 函数即没有参数,也没有返回类型(与 C 家族中的其它语言恰好相反)。如果你不小心为 main 函数添加了参数或者返回类型,将会引发构建错误: func main must have no arguments and no return values results. 在程序开始执行并完成初始化后,第一个调用(程序的入口点)的函数是 `main.main()`(如:C 语言),该函数一旦返回就表示程序已成功执行并立即退出。 函数里的代码(函数体)使用大括号 `{ }` 括起来。 左大括号 `{` 必须与方法的声明放在同一行,这是编译器的强制规定,否则你在使用 gofmt 时就会出现 `build-error: syntax error: unexpected semicolon or newline before {` 这样的错误提示。 (这是因为编译器会产生 `func main() ;` 这样的结果,很明显这错误的) ( ***Go 语言虽然看起来不使用分号作为语句的结束,但实际上这一过程是由编译器自动完成,因此才会引发向上面这样的错误*** ) 右大括号 `}` 需要被放在紧接着函数体的下一行。如果你的函数非常简短,你也可以将它们放在同一行:`func Sum(a, b int) int { return a + b }`。 对于大括号 `{ }` 的使用规则在任何时候都是相同的(如:if 语句等)。 因此符合规范的函数一般写成如下的形式: func functionName(parameter_list) (return_value_list) { … } 其中: parameter_list 的形式为 (param1 type1, param2 type2, …) return_value_list 的形式为 (ret1 type1, ret2 type2, …) 只有当某个函数需要被外部包调用的时候才使用大写字母开头,并遵循 Pascal 命名法;否则就遵循骆驼命名法,即第一个单词的首字母小写,其余单词的首字母大写。 `fmt.Println("hello, workd")` 这一行调用了 `fmt` 包中的 `Println` 函数,可以将字符串输出到控制台,并在最后自动增加换行字符 `\n`。 使用 `fmt.Print("hello, world\n")` 可以得到相同的结果。 `Print` 和 `Println` 这两个函数也支持使用变量,如:`fmt.Println(arr)`。如果没有特别指定,它们会以默认的打印格式将变量 `arr` 输出到控制台。 单纯地打印一个字符串或变量甚至可以使用预定义的方法来实现,如:`print`、`println:print("ABC")`、`println("ABC")`、`println(i)`(带一个变量 i)。 这些函数只可以用于调试阶段,在部署程序的时候务必将它们替换成 `fmt` 中的相关函数。 当被调用函数的代码执行到结束符 `}` 或返回语句时就会返回,然后程序继续执行调用该函数之后的代码。 程序正常退出的代码为 0 `Program exited with code 0`;如果程序因为异常而被终止,则会返回非零值,如:1。这个数值可以用来测试是否成功执行一个程序。 ##4.2.3 注释QUESTION Example 4.2 [hello_world2.go](examples/chapter_4/hello_world2.go) package main import "fmt" // Package implementing formatted I/O. func main() { fmt.Printf("Καλημέρα κόσμε; or こんにちは 世界\n") } 这说明了国际化字符 Καλημέρακόσμε; or こんにちは世界,可以打印,也可以用作注释使用。 注释肯定不会被编译,但是他们被 godoc使用(参见 § 3.6) 行注释以`//`开始,可以在一行的开头或其他地方;这是最经常被使用的。多行注释也叫叫块注释以`/*`开始,以`*/`结束,不允许嵌套;这是用来包文档和注释代码。 每个包应该有包注释,注释块紧接 package 语句,介绍这个包并且提供相关信息,作整体功能介绍。一个包可以分散在多个文件中,但是注释只需要写在其中一个。当一个开发人员需要包的信息,使用 godoc,这些注释将被显示。其后的句子和段落可以给出更多的细节。注释句子应适当的空行。 例子: // Package superman implements methods for saving the world. // // Experience has shown that a small number of procedures can prove // helpful when attempting to save the world. package superman 几乎每一个顶层的类型,常数,变量,函数和每一个导出名都应该有注释。此注释(称为文档注释)出现在函数前面,如函数 Abcd 以:“Abcd...”开始。 例子: // enterOrbit causes Superman to fly into low Earth orbit, a position // that presents several possibilities for planet salvation. func enterOrbit() error { ... } godoc-tool (参见 §3.6) 收集这些注释并产生一个技术文档。 ##4.2.4 类型 变量(如常量)保持数据,数据可以是不同数据类型或者短整型。使用 var 声明的变量自动初始化为它的零值。类型定义了一系列值及操作,可以填充这些值。 类型可以是基本的(或原生的),如 int,float,bool,string,或结构的(或复合的),如 struct,array,slice,map,channel和insterface,他们只描述了类型的行为。 结构化的类型没有真正的值时,它的值是 nil,这是这些类型的默认值(在 Objective-C 中是 nil,在 Java 中是 null,在 C 和 C++ 中是NULL或 0 )。Go 中没有类型继承。 函数也可以是一个确定的类型,就是使用函数返回一个类型。返回类型写在函数名称和操作参数列表后面,如: func FunctionName (a typea, b typeb) typeFunc 返回 typeFunc 类型的变量 var 可以在函数的某处声明: return var 函数可以返回多个变量,并且返回类型使用逗号分隔和小括号`( )`包围,如:func FunctionName (a typea, b typeb) (t1 type1, t2 type2) 列子: 函数 Atoi (参见 § 4.7): func Atoi(s string) (i int, err error) 返回的形式为: return var1, var2 这是经常当成功(真/假)执行一个函数或错误信息与返回值一起返回时使用(见下面的多分配)。 使用 type 关键字定义你自己的类型。然后你可能像定义一个结构体(参见 第10章),但是也可能定义一个存在的类型的别名,如: type IZ int 然后我们可以定义变量如:var a IZ = 5 我们说 a 是 int 的基本类型,这使转型成为可能(参见 § 4.2.6)。 如果你有多个类型需要定义,可以使用多关键字形式,如: type ( IZ int FZ float STR string ) 每个值经过编译后必须有类型(编译器必须能够推断出所有值的类型): Go 语言是一种静态类型语言. ##4.2.5 Go 编程的一般结构 下面的程序可以编译,但并没有什么用处,但展示 Go 程序的首选结构。这种结构不是必须的,编译器不关心main()或变量声明在最后,但一个标准的结构使代码从上到下有更好的可读性。 所有结构将在这接下来的章节进一步解释说,但总体思路是: - import 之后: 定义常量,变量和类型 - 如果需要,然后是 init() 函数: 这是一个特殊的功能,每一个包可以包含并首先执行。 - 然后是 main() 函数(只在 package main 中) - 然后是剩下的函数,首先是类型的方法,或是 main() 函数中先后顺序调用的函数;或方法和功能,按字母顺序的方法和函数排序很高的。 Example 4.4 [gotemplate.go](examples/chapter_4/gotemplate.go) package main import ( “fmt” ) const c = “C” var v int = 5 type T struct{} func init() { // initialization of package } func main() { var a int Func1() // ... fmt.Println(a) } func (t T) Method1() { //... } func Func1() { // exported function Func1 //... } Go 程序的执行(程序启动)顺序如下: (1)在 package main 中的包全部导入,按标明的顺序: (2)如果导入包,(1)被这个包调用(递归),但是一个确定的包只会被导入一次 (3)然后每一个包(以相反的顺序)中所有的常量和变量都被评估,如果它包含这个函数,将执行 init() 方法。 (4)最后在 package main 中也一样,然后 main() 函数开始执行。 ##4.2.6 转型 如果必要和可能的值可以转型(转换, 包裹)成另一种类型的值。Go 语言没有隐式转换(自动),它必须明确说明,使用一个类似函数调用的语法(这里的类型可以看作是一种函数): valueOfTypeB = typeB(valueOfTypeA) 类型B的值 = 类型B(类型A的值) 例子: a := 5.0 b := int(a) 但这只能在定义正确的情况下成功,例如从一个窄型转换到一个宽型(例如:int16 到 int32)。当从一个宽型转换到窄型(例如:int32 到 int16 或 float32 到 int)值丢失(截断)会发生。当无法转型并且编译器检测到了这一点,会给出一个编译错误,否则发生运行时错误。 变量是相同的基本类型可以相互转型: var a IZ = 5 c := int(a) d := IZ(c) ##4.2.7 关于 Go 语言命名 干净,可读的代码和简洁性是 Go 开发的主要目标。gofmt 命令强化了代码风格。在 Go 语言中命名应该是简短的,简洁的,令人回味的。长名称命名使用大小写混合和下划线,像在 Java 或 Python 代码中那样,往往阻碍了可读性。名称不应包含包名:包名已经足够说明了。一个方法或函数返回的对象,其中函数被作为一个名词的名字命名,不应使用 Get...。若要更改一个对象,请使用“SetName”。如果有必要,Go 语言使用大小写混合 MixedCaps 或 mixedCaps,而不是下划线连接多个名称。 ##链接 - [目录](directory.md) - 上一部分:[文件名、关键字与标识符](04.1.md) - 下一节:[常量](04.3.md)