Go程序的基本结构和要素
先看一个示例,就是我们安装测试Go 的时候写的 hello_world.go
,不过稍微加了一点东西
package main
import "fmt"
func main(){
fmt.Println("hello, world")
}
包的概念,导入和可见性
包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。
每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go
为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。
必须在源文件中非注释的第一行指明这个文件属于哪个包。
比如上例子 中的package main
。package main
表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main
的包。
一个应用程序可以包含不同的包,你可以用一些较小的文件,并且在每个文件非注释的第一行都使用 package main
来指明这些文件都属于 main 包。如果你打算编译包名不是为 main 的源文件,如 pack1
,编译后产生的对象文件将会是 pack1.a
而不是可执行程序。 所有的包名都应该使用小写字母 。
标准库
在 Windows 下的位置在 Go 根目录下的子目录 pkg\windows_386
中;
在 Linux 下的标准库在 Go 根目录下的子目录 pkg\linux_amd64
中(如果是安装的是 32 位,则在 linux_386
目录中)。
一般情况下,标准包会存放在 $GOROOT/pkg/$GOOS_$GOARCH/
目录下。
如果想要构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序。
属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。
如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。
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"
也可以这样优雅的书写
import (
"fmt"
"os"
)
同样可以写到一行中(像下面这样写了之后,使用 gofmt 后将会被强制换行)
import ("fmt"; "os")
习惯:导入多个包时,最好按照字母顺序排列包名,这样做更加清晰易读。
如果包名不是以 .
或 /
开头,如 "fmt"
或者 "container/list"
,则 Go 会在全局文件进行查找;如果包名以 ./
开头,则 Go 会在相对目录中查找;如果包名以 /
开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找。
注: 以相对路径在GOPATH下导入包会产生报错信息 (报错信息:local import “./XXX” in non-local package)
除了符号 _
,包中所有代码对象的标识符必须是唯一的,以避免名称冲突。但是相同的标识符可以在不同的包中使用,因为可以使用包名来区分它们。
可见性规则
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。
(注:大写字母可以使用任何 Unicode 编码的字符,比如希腊文,不仅仅是 ASCII 码中的大写字母)
举例:假设在包 pack1 中我们有一个变量或函数叫做 Thing(以 T 开头,所以它能够被导出),那么在当前包中导入 pack1 包,Thing 就可以像面向对象语言那样使用点标记来调用:pack1.Thing
(pack1 在这里是不可以省略的)。因此包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):例如 pack1.Thing
和 pack2.Thing
。
对包使用 别名 ,如:import fm "fmt"
。
注意事项
如果你导入了一个包却没有使用它,则会在构建程序时引发错误,如 imported and not used: os
。
包的分级声明和初始化
你可以在使用 import
导入包之后定义或声明 0 个或多个常量(const)、变量(var)和类型(type),这些对象的作用域都是全局的(在本包范围内),所以可以被本包中所有的函数调用(如 gotemplate.go 源文件中的 c 和 v),然后声明一个或多个函数(func)。
函数
定义一个函数最简单的格式:
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()
。该函数一旦返回就表示程序已成功执行并立即退出。
函数里的代码(函数体)使用大括号 {}
括起来。
左大括号 {
必须与方法的声明放在同一行,这是编译器的强制规定,否则你在使用 gofmt 时就会报错。
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
函数,可以将字符串输出到控制台,并在最后自动增加换行字符 \n
:
fmt.Println("hello, world")
使用 fmt.Print("hello, world\n")
可以得到相同的结果。
Print
和 Println
这两个函数也支持使用变量。这些函数只可以用于调试阶段,在部署程序的时候务必将它们替换成 fmt
中的相关函数。
程序正常退出的代码为 0 即 Program exited with code 0
;如果程序因为异常而被终止,则会返回非零值,如:1。这个数值可以用来测试是否成功执行一个程序。
注释
示例: hello_world2.go
package main
import "fmt" // Package implementing formatted I/O.
func main() {
fmt.Printf("Καλημέρα κόσμε; or こんにちは 世界\n")
}
使用以 //
开头的单行注释。多行注释也叫块注释,均已以 /*
开头,并以 */
结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
注释不会被编译,但可以通过 godoc 来使用。
每一个包应该有相关注释,在 package 语句之前的块注释将被默认认为是这个包的文档说明,其中应该提供一些相关信息并对整体功能做简要的介绍。一个包可以分散在多个文件中,但是只需要在其中一个进行注释说明即可。
在首行的简要注释之后可以用成段的注释来进行更详细的说明,另外,在多段注释之间应以空行分隔加以区分。
示例:
// 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 {
...
}
类型
Golang的变量使用 var 声明的变量的值会自动初始化为该类型的零值。类型定义了某个变量的值的集合与可对其进行操作的集合。
- 基本类型:int、float、bool、string
- 结构化类型(复杂类型):struct、array、slice、map、channel
- 只描述行为的类型:interface
函数也是一个确定的类型,以函数作为返回类型。需要写在函数名和可选的参数之后,例如
func FunctionName(a typea, b typeb) typeFunc
也可以返回多个,需要用小括号把它们括起来,并且使用逗号分隔
func FunctionName(a typea, b typeb) (t1 type1, t2 type2)
那么如上返回的形式为
return var1, var2
使用 type 关键字可以定义一个类型,它可以是结构体,也可以是一个已经存在的类型的别名
type IZ int
也可以同时定义多个类型
type (
IZ int
FZ float64
STR string
)
go的编译器要求能够推断出所有值的对应类型出来,所以 Go 是一种强静态的语言。
类型转换
一个类型的值可以被转换为另一种类型的值。因为 Go 不存在隐式转换,所以转换必须显式说明(类型在这里的作用可以被看成是一个函数)
valueOfTypeB = typeB(valueOfTypeA)
类型 B 的值 = 类型 B(类型 A 的值) ,举个例子
a := 5.0
b := int(a)
var a IZ = 5
c := int(a)
d := IZ(c)
只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(例如将 int16 转换为 int32)。当从一个取值范围较大的转换到取值范围较小的类型时(例如将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失(截断)的情况。当编译器捕捉到非法的类型转换时会引发编译时错误,否则将引发运行时错误。
命名规范
通过 gofmt 来强制实现统一的代码风格。名称不需要指出自己所属的包,因为在调用的时候会使用包名作为限定符。返回某个对象的函数或方法的名称一般都是使用名词,没有 Get...
之类的字符,如果是用于修改某个对象,则使用 SetName
。有必须要的话可以使用大小写混合的方式,如 MixedCaps 或 mixedCaps,而不是使用下划线来分割多个名称。