mirror of
https://github.com/unknwon/the-way-to-go_ZH_CN.git
synced 2025-08-12 03:55:28 +08:00
311 lines
15 KiB
Markdown
311 lines
15 KiB
Markdown
#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`开头。如果编译一个包名不是`package 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 依赖 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”
|
||
|
||
但是更短并且更优雅的方法(称为_factoring the keyword_,同样适用于const,var 和 type)也是有的:
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
)
|
||
|
||
(它还可以更短:`import ("fmt", "os")`但是对于分发版本,gofmt后会强制换行)
|
||
|
||
当需要导入多个包时,它会按字母顺序排列包名。
|
||
|
||
包名如果不是以`.`或`/`开头,如"fmt"或者"container/list",Go 会在全局的 Go 树进行查找。包名如果以`./`开头,Go 会在实际的目录中搜索;以`/`开始(即使在Windows中),将会在系统(绝对)路径中搜索。
|
||
|
||
包已经包含了其他的所有引用的代码对象。
|
||
|
||
除了`_`,代码对象的标识符必须在包中唯一:这样就没有_名称冲突_了。但是相同的标识符可以用在不同的包中:_包名保证_了它们的不同。
|
||
|
||
包暴露自己的代码对象到包外是根据以下的规则,
|
||
这是由编译器执行:
|
||
|
||
___可见行规则___:
|
||
|
||
当标识符(包括常量,变量,类型,函数名,结构体字段,...)以一个大写字母开头,如 Group1,那么‘对象’的这个方法在包外是可见的(对客户端程序可见,需要导入相应的包),这被叫做被导出(像面向对象语言的 public )。标识符如果以小写字母开头,对包外是不可见的,但是他们在整个包内是可以见并可用的(像面向对象语言的 private )。
|
||
|
||
(大写字母可以来自整个Unicode编码范围,像希腊文,不仅ASCII中的大写字母)。
|
||
|
||
因此,导入一个包(只)能够使该包中的导出对象被访问。
|
||
|
||
假设在包 pack1 中我们有一个 thing (变量或函数)叫做 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")
|
||
}
|
||
|
||
_提醒_:导入一个包,但是没有在代码中使用会导致一个构建错误(例如:导入但补使用的: os 包)。这遵循 Go 的格言:“没有不必要的代码!“
|
||
|
||
包级声明和初始化:
|
||
|
||
import 语句之后可以声明 0 个或多个常量(const),变量(var)和类型(type),这些都是全局的(在包范围内),并且代码中所有的函数可以调用(如 c 和 v 在下面的 gotemplate.go 中),并且它们后面跟着一个或多个函数(func)。
|
||
|
||
##4.2.2 函数
|
||
|
||
最简单的函数定义使用这样的格式:func functionName()
|
||
|
||
小括号`()`中,没有,或者有一个或多个参数(使用`,`分离)可以作为该函数的输入。每个参数的变量名后,一定要是该参数的类型。
|
||
|
||
一个主函数,作为程序启动所必需的(通常是第一个函数),否则会产生构建未定义错误:main.main 发生。主函数没有参数和返回类型(与 C 语言家族相反),否则,将得到的构建错误:
|
||
|
||
主函数必须没有参数和返回值
|
||
|
||
当程序执行,初始化结束后,第一个调用(程序的入口点)的函数是 main.main()(如 C 语言)。程序在 main.main 结束后立即并成功退出。
|
||
|
||
方法里的代码(方法体)使用大括号:`{ }`包含。
|
||
|
||
第一个`{`必须与方法声明在同一行:这被编译器强制判断,否则 gofmt 会有(build-error: syntax error: unexpected semicolon or newline before {)的错误提示。
|
||
|
||
(这是应为编译器执行 `func main() ;` 这是一个错误)
|
||
|
||
最后的`}`将被定位在函数代码下面;小功能是允许的一切都写在一行,例如:`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命名规则 PascalCasing,否则遵循砣峰命名规则 camelCasing:首单词字母小写,每一个新词的名称以大写字母开头。
|
||
|
||
行:`fmt.Println("hello, workd")`调用 fmt 包的 Println 函数,将输出字符串到控制台,接着换行字符`\n`。
|
||
|
||
`fmt.Print("hello, world\n")` 可以得到相同的结果。
|
||
|
||
Print 和 Println 方法也支持变量,如:fmt.Println(arr);他们使用默认的输出格式输出 arr 变量。
|
||
|
||
打印一个字符串或变量甚至可以使用预定义的方法 print 和 println:print("ABC") 或 println("ABC")或者(带一个变量 i):println(i)
|
||
|
||
这些仅仅是用于在调试阶段;部署程序时会将它们替换 fmt 的相关函数。
|
||
|
||
当遇到结束符`}`或返回语句,程序根据方法的调用继续执行。
|
||
|
||
程序正常退出代码为 0 (程序以代码 0 退出);程序以异常中止退出的使用另外一个整数代码如 1 ;这可以用来测试成功执行一个程序。
|
||
|
||
##4.2.3 注释
|
||
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)
|