diff --git a/eBook/04.2.md b/eBook/04.2.md index 2a53f02..32b27fa 100644 --- a/eBook/04.2.md +++ b/eBook/04.2.md @@ -1,14 +1,16 @@ # 4.2 Go 程序的基本结构和要素 -Example 4.1 [hello_world.go](examples/chapter_4/hello_world.go) +示例 4.1 [hello_world.go](examples/chapter_4/hello_world.go) + +```go +package main - package main - - import "fmt" - - func main() { - fmt.Println("hello, world") - } +import "fmt" + +func main() { + fmt.Println("hello, world") +} +``` ## 4.2.1 包的概念、导入与可见性 @@ -24,13 +26,13 @@ Example 4.1 [hello_world.go](examples/chapter_4/hello_world.go) 在 Go 的安装文件里包含了一些可以直接使用的包,即标准库。在 Windows 下,标准库的位置在 Go 根目录下的子目录 `pkg\windows_386` 中;在 Linux 下,标准库在 Go 根目录下的子目录 `pkg\linux_amd64` 中(如果是安装的是 32 位,则在 `linux_386` 目录中)。一般情况下,标准包会存放在 `$GOROOT/pkg/$GOOS_$GOARCH/` 目录下。 -Go 的标准库包含了大量的包(如:fmt,os),但是你也可以创建自己的包(第 8 章)。 +Go 的标准库包含了大量的包(如:fmt 和 os),但是你也可以创建自己的包(第 8 章)。 如果想要构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序。 属于同一个包的源文件必须全部被一起编译,一个包既是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。 -*如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。* +**如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。** Go 中的包模型采用了显式依赖关系的机制来达到快速编译的目的,编译器会从后缀名为 `.o` 的对象文件(需要且只需要这个文件)中提取传递依赖类型的信息。 @@ -41,33 +43,43 @@ Go 中的包模型采用了显式依赖关系的机制来达到快速编译的 这种机制对于编译大型的项目时可以显著地提升编译速度。 -*每一段代码只会被编译一次* +**每一段代码只会被编译一次** 一个 Go 程序是通过 `import` 关键字将一组包链接在一起。 -`import “fmt”` 告诉 Go 编译器这个程序需要使用 `fmt` 包(的函数,或其他元素),`fmt` 包实现了格式化 IO(输入/输出)的函数。包名被封闭在半角双引号 `""` 中。如果你打算从已编译的包中导入并加载公开声明的方法,不需要插入已编译包的源代码。 +`import "fmt"` 告诉 Go 编译器这个程序需要使用 `fmt` 包(的函数,或其他元素),`fmt` 包实现了格式化 IO(输入/输出)的函数。包名被封闭在半角双引号 `""` 中。如果你打算从已编译的包中导入并加载公开声明的方法,不需要插入已编译包的源代码。 如果需要多个包,它们可以被分别导入: - - import "fmt" - import "os" + +```go +import "fmt" +import "os" +``` 或: - - import "fmt"; import "os" + +```go +import "fmt"; import "os" +``` 但是还有更短且更优雅的方法(被称为因式分解关键字,该方法同样适用于 const、var 和 type 的声明或定义): + +```go +import ( + "fmt" + "os" +) +``` - import ( - "fmt" - "os" - ) - -(它甚至还可以更短:`import ("fmt"; "os")` 但使用 gofmt 后将会被强制换行) +它甚至还可以更短的形式,但使用 gofmt 后将会被强制换行: + +```go +import ("fmt"; "os") +``` 当你导入多个包时,导入的顺序会按照字母排序。 -如果包名不是以 `.` 或 `/` 开头,如 "fmt" 或者 "container/list" ,则 Go 会在全局文件进行查找;如果包名以 `./` 开头,则 Go 会在相对目录中查找;如果包名以 `/` 开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找。 +如果包名不是以 `.` 或 `/` 开头,如 `"fmt"` 或者 `"container/list"`,则 Go 会在全局文件进行查找;如果包名以 `./` 开头,则 Go 会在相对目录中查找;如果包名以 `/` 开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找。 导入包即等同于包含了这个包的所有的代码对象。 @@ -83,22 +95,27 @@ Go 中的包模型采用了显式依赖关系的机制来达到快速编译的 因此,在导入一个外部包后,能够且只能够访问该包中导出的对象。 -假设在包 pack1 中我们有一个变量或函数叫做 Thing(以 T 开头,所以它能够被导出),那么在当前包中导入 pack1 包,Thing 就可以像面向对象语言那样使用点标记来调用:`pack1.Thing`(pack1 在这里是不可以省略的) +假设在包 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) +示例 4.2 [alias.go](examples/chapter_4/alias.go) + +```go +package main + +import fm "fmt" // alias3 - package main - import fm "fmt" // alias3 - - func main() { - fm.Println("hello, world") - } +func main() { + fm.Println("hello, world") +} +``` -**注意事项** 如果你导入了一个包却没有使用它,则会在构建程序时引发错误,如 `imported and not used: os`,这正是遵循了 Go 的格言:“没有不必要的代码!“ +**注意事项** + +如果你导入了一个包却没有使用它,则会在构建程序时引发错误,如 `imported and not used: os`,这正是遵循了 Go 的格言:“没有不必要的代码!“。 **包的分级声明和初始化** @@ -106,41 +123,58 @@ Example 4.2 [alias.go](examples/chapter_4/alias.go) ## 4.2.2 函数 -这是定义一个函数最简单的格式:`func functionName()` +这是定义一个函数最简单的格式: + +```go +func functionName() +``` 你可以在括号 `()` 中写入 0 个或多个函数的参数(使用逗号 `,` 分隔),每个参数的名称后面必须紧跟着该参数的类型。 -main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数( ***译者注:如果有 init() 函数则会先执行该函数*** )。如果你的 main 包的源代码没有包含 main 函数,则会引发构建错误 `undefined: main.main`。main 函数即没有参数,也没有返回类型(与 C 家族中的其它语言恰好相反)。如果你不小心为 main 函数添加了参数或者返回类型,将会引发构建错误: +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 {` 这样的错误提示。 +左大括号 `{` 必须与方法的声明放在同一行,这是编译器的强制规定,否则你在使用 gofmt 时就会出现错误提示: + + `build-error: syntax error: unexpected semicolon or newline before {` (这是因为编译器会产生 `func main() ;` 这样的结果,很明显这错误的) -( ***Go 语言虽然看起来不使用分号作为语句的结束,但实际上这一过程是由编译器自动完成,因此才会引发向上面这样的错误*** ) + +**Go 语言虽然看起来不使用分号作为语句的结束,但实际上这一过程是由编译器自动完成,因此才会引发向上面这样的错误** -右大括号 `}` 需要被放在紧接着函数体的下一行。如果你的函数非常简短,你也可以将它们放在同一行:`func Sum(a, b int) int { return a + b }`。 +右大括号 `}` 需要被放在紧接着函数体的下一行。如果你的函数非常简短,你也可以将它们放在同一行: + +```go +func Sum(a, b int) int { return a + b } +``` -对于大括号 `{ }` 的使用规则在任何时候都是相同的(如:if 语句等)。 +对于大括号 `{}` 的使用规则在任何时候都是相同的(如:if 语句等)。 因此符合规范的函数一般写成如下的形式: - - func functionName(parameter_list) (return_value_list) { - … - } + +```go +func functionName(parameter_list) (return_value_list) { + … +} +``` 其中: - parameter_list 的形式为 (param1 type1, param2 type2, …) - return_value_list 的形式为 (ret1 type1, ret2 type2, …) +- parameter_list 的形式为 (param1 type1, param2 type2, …) +- return\_value_list 的形式为 (ret1 type1, ret2 type2, …) 只有当某个函数需要被外部包调用的时候才使用大写字母开头,并遵循 Pascal 命名法;否则就遵循骆驼命名法,即第一个单词的首字母小写,其余单词的首字母大写。 -`fmt.Println("hello, workd")` 这一行调用了 `fmt` 包中的 `Println` 函数,可以将字符串输出到控制台,并在最后自动增加换行字符 `\n`。 +下面这一行调用了 `fmt` 包中的 `Println` 函数,可以将字符串输出到控制台,并在最后自动增加换行字符 `\n`: + +```go +fmt.Println("hello, workd") +``` 使用 `fmt.Print("hello, world\n")` 可以得到相同的结果。 @@ -152,43 +186,51 @@ main 函数是每一个可执行程序所必须包含的,一般来说都是在 当被调用函数的代码执行到结束符 `}` 或返回语句时就会返回,然后程序继续执行调用该函数之后的代码。 -程序正常退出的代码为 0 `Program exited with code 0`;如果程序因为异常而被终止,则会返回非零值,如:1。这个数值可以用来测试是否成功执行一个程序。 +程序正常退出的代码为 0 即 `Program exited with code 0`;如果程序因为异常而被终止,则会返回非零值,如:1。这个数值可以用来测试是否成功执行一个程序。 ## 4.2.3 注释 -Example 4.2 [hello_world2.go](examples/chapter_4/hello_world2.go) +示例 4.2 [hello_world2.go](examples/chapter_4/hello_world2.go) + +```go +package main - package main - import "fmt" // Package implementing formatted I/O. - func main() { - fmt.Printf("Καλημέρα κόσμε; or こんにちは 世界\n") - } +import "fmt" // Package implementing formatted I/O. + +func main() { + fmt.Printf("Καλημέρα κόσμε; or こんにちは 世界\n") +} +``` 上面这个例子通过打印 `Καλημέρα κόσμε; or こんにちは 世界` 展示了如何在 Go 中使用国际化字符,以及如何使用注释。 -注释不会被编译,但可以通过 godoc 来使用(第 3.6 节) +注释不会被编译,但可以通过 godoc 来使用(第 3.6 节)。 单行注释是最常见的注释形式,你可以在任何地方使用以 `//` 开头的单行注释。多行注释也叫块注释,均已以 `/*` 开头,并以 `*/` 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。 每一个包应该有相关注释,在 package 语句之前的块注释将被默认认为是这个包的文档说明,其中应该提供一些相关信息并对整体功能做简要的介绍。一个包可以分散在多个文件中,但是只需要在其中一个进行注释说明即可。当开发人员需要了解包的一些情况时,自然会用 godoc 来显示包的文档说明,在首行的简要注释之后可以用成段的注释来进行更详细的说明,而不必拥挤在一起。另外,在多段注释之间应以空行分隔加以区分。 示例: + +```go +// 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 +``` - // 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..." 作为开头。 +几乎所有全局作用域的类型、常量、变量、函数和被导出的对象都应该有一个合理的注释。如果这种注释(称为文档注释)出现在函数前面,例如函数 Abcd,则要以 `"Abcd..."` 作为开头。 示例: - - // enterOrbit causes Superman to fly into low Earth orbit, a position - // that presents several possibilities for planet salvation. - func enterOrbit() error { - ... - } + +```go +// enterOrbit causes Superman to fly into low Earth orbit, a position +// that presents several possibilities for planet salvation. +func enterOrbit() error { + ... +} +``` godoc 工具(第 3.6 节)会收集这些注释并产生一个技术文档。 @@ -196,43 +238,63 @@ godoc 工具(第 3.6 节)会收集这些注释并产生一个技术文档。 可以包含数据的变量(或常量)可以使用不同的数据类型或类型来保存数据。使用 var 声明的变量的值会自动初始化为该类型的零值。类型定义了某个变量的值的集合与可对其进行操作的集合。 -类型可以是基本类型,如:int,float,bool,string;结构化的(复合的),如:struct,array,slice,map,channel;只描述类型的行为的,如:interface。 +类型可以是基本类型,如:int、float、bool、string;结构化的(复合的),如:struct、array、slice、map、channel;只描述类型的行为的,如:interface。 结构化的类型没有真正的值,它使用 nil 作为默认值(在 Objective-C 中是 nil,在 Java 中是 null,在 C 和 C++ 中是NULL或 0)。值得注意的是,Go 语言中不存在类型继承。 函数也可以是一个确定的类型,就是以函数作为返回类型。这种类型的声明要写在函数名和可选的参数列表之后,例如: - - func FunctionName (a typea, b typeb) typeFunc + +```go +func FunctionName (a typea, b typeb) typeFunc +``` 你可以在函数体中的某处返回使用类型为 typeFunc 的变量 var: + +```go +return var +``` - return var - -一个函数可以拥有多返回值,返回类型之间需要使用逗号分割,并使用小括号 `( )` 将它们括起来,如:`func FunctionName (a typea, b typeb) (t1 type1, t2 type2)` +一个函数可以拥有多返回值,返回类型之间需要使用逗号分割,并使用小括号 `()` 将它们括起来,如: + +```go +func FunctionName (a typea, b typeb) (t1 type1, t2 type2) +``` 示例: 函数 Atoi (第 4.7 节):`func Atoi(s string) (i int, err error)` -返回的形式:`return var1, var2` +返回的形式: + +```go +return var1, var2 +``` 这种多返回值一般用于判断某个函数是否执行成功(true/false)或与其它返回值一同返回错误消息(详见之后的并行赋值)。 使用 type 关键字可以定义你自己的类型,你可能想要定义一个结构体(第 10 章),但是也可以定义一个已经存在的类型的别名,如: + +```go +type IZ int +``` - type IZ int +**这里并不是真正意义上的别名,因为使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换。** -( **译者注:这里并不是真正意义上的别名,因为使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换** ) - -然后我们可以像 `var a IZ = 5` 这样声明变量。 +然后我们可以使用下面的方式声明变量: + +```go +var a IZ = 5 +``` 这里我们可以看到 int 是变量 a 的底层类型,这也使得它们之间存在相互转换的可能(第 4.2.6 节)。 如果你有多个类型需要定义,可以使用因式分解关键字的方式,例如: - - type ( - IZ int - FZ float - STR string - ) + +```go +type ( + IZ int + FZ float64 + STR string +) +``` 每个值都必须在经过编译后属于某个类型(编译器必须能够推断出所有值的类型),因为 Go 语言是一种静态类型语言。 @@ -247,29 +309,39 @@ godoc 工具(第 3.6 节)会收集这些注释并产生一个技术文档。 - 如果当前包是 main 包,则定义 main 函数。 - 然后定义其余的函数,首先是类型的方法,接着是按照 main 函数中先后调用的顺序来定义相关函数,如果有很多函数,则可以按照字母顺序来进行排序。 -Example 4.4 [gotemplate.go](examples/chapter_4/gotemplate.go) +示例 4.4 [gotemplate.go](examples/chapter_4/gotemplate.go) + +```go +package main - 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 - //... - } +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 程序的执行(程序启动)顺序如下: @@ -281,27 +353,33 @@ Go 程序的执行(程序启动)顺序如下: ## 4.2.6 类型转换 在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于 Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明,就像调用一个函数一样(类型在这里的作用可以看作是一种函数): + +```go +valueOfTypeB = typeB(valueOfTypeA) +``` - valueOfTypeB = typeB(valueOfTypeA) - -( **译者注:类型 B 的值 = 类型 B(类型 A 的值)** ) +**类型 B 的值 = 类型 B(类型 A 的值)** 示例: - - a := 5.0 - b := int(a) + +```go +a := 5.0 +b := int(a) +``` 但这只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(例如将 int16 转换为 int32)。当从一个取值范围较大的转换到取值范围较小的类型时(例如将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失(截断)的情况。当编译器捕捉到非法的类型转换时会引发编译时错误,否则将引发运行时错误。 具有相同底层类型的变量之间可以相互转换: - - var a IZ = 5 - c := int(a) - d := IZ(c) + +```go +var a IZ = 5 +c := int(a) +d := IZ(c) +``` ## 4.2.7 Go 命名规范 -干净、可读的代码和简洁性是 Go 追求的主要目标。通过 gofmt 来强制实现统一的代码风格。Go 语言中对象的命名也应该是简洁且有意义的。像 Java 和 Python 中那样使用混合着大小写和下划线的冗长的名称会严重降低代码的可读性。名称不需要指出自己所属的包,因为在调用的时候会使用包名作为限定符。返回某个对象的函数或方法的名称一般都是使用名词,没有 “Get...” 之类的字符( **译者注:个人觉得这个有点牵强** ),如果是用于修改某个对象,则使用 “SetName”。有必须要的话可以使用大小写混合的方式,如 MixedCaps 或 mixedCaps,而不是使用下划线来分割多个名称。 +干净、可读的代码和简洁性是 Go 追求的主要目标。通过 gofmt 来强制实现统一的代码风格。Go 语言中对象的命名也应该是简洁且有意义的。像 Java 和 Python 中那样使用混合着大小写和下划线的冗长的名称会严重降低代码的可读性。名称不需要指出自己所属的包,因为在调用的时候会使用包名作为限定符。返回某个对象的函数或方法的名称一般都是使用名词,没有 `Get...` 之类的字符,如果是用于修改某个对象,则使用 `SetName`。有必须要的话可以使用大小写混合的方式,如 MixedCaps 或 mixedCaps,而不是使用下划线来分割多个名称。 ## 链接