赞
踩
Go
语言是使用包来组织源代码的,并实现命名空间的管理。任何源代码文件必须属于某个包。源码文件的第一行有效代码必须是 package pacakgeName
语句,通过该语句声明自己所在的包。
所有的 .go
文件,除了空行和注释,都应该在第一行声明自己所属的包。即所有代码都必须组织在 package 中。包的结构特点有:
package
声明包名称;main
、 all
、 std
这三个保留名称;package main
和入口函数 main
, main
包是 Go
语言程序的入口包,一个 Go
语言程序必须有且仅有一个 main
包,并且,一个 main
包中也必须有且仅有一个 main
函数。如果一个程序没有 main
包,那么编译时将会出错,无法生成可执行文件;.go
文件必须声明同一个包名;包中成员以名称首字母大小写决定访问权限。
Public
: 首字母大写,可被包外访问;internal
: 首字母小写,仅包内成员可以访问;该规则适用于全局变量、全局常量、类型、结构字段、函数、方法等。
给包命名的惯例是使用包所在目录的名字。给包及其目录命名时,应该使用简洁、清晰且全小写的名字,这有利于开发时频繁输入包名。
记住,并不需要所有包的名字都与别的包不同,因为导入包时是使用全路径的,所以可以区分同名的不同包。一般情况下,包被导入后会使用你的包名作为默认的名字,不过这个导入后的名字可以修改。这个特性在需要导入不同目录的同名包时很有用。
关于默认包名一般采用导入路径名的最后一段的约定也有三种例外情况。
main
包,这时候 main
包本身的导入路径是无关紧要的。名字为 main
的包是给 go build
构建命令一个信息,这个包编译完之后必须调用连接器生成一个可执行程序。_test.go
为后缀的 Go
源文件(译注:前面必须有其它的字符,因为以 _
或 .
开头的源文件会被构建工具忽略),并且这些源文件声明的包名也是以 _test
为后缀名的。这种目录可以包含两种包:一种是普通包,另一种则是测试的外部扩展包。所有以 _test
为后缀包名的测试外部扩展包都由 go test
命令独立编译,普通包和测试的外部扩展包是相互独立的。测试的外部扩展包一般用来避免测试代码中的循环导入依赖。yaml
。在 Go
语言里,命名为 main
的包具有特殊的含义。 Go
语言的编译程序会试图把这种名字的包编译为二进制可执行文件。所有用 Go
语言编译的可执行程序都必须有一个名叫 main
的包。
当编译器发现某个包的名字为 main
时,它一定也会发现名为 main()
的函数,否则不会创建可执行文件。 main()
函数是程序的入口,所以,如果没有这个函数,程序就没有办法开始执行。
程序编译时,会使用声明 main
包代码所在目录的目录名作为二进制可执行文件的文件名。
而且通常来说,main
包应该很简洁。我们在 main
包中会做一些命令行参数解析、资源初始化、日志设施初始化、数据库连接初始化等工作,之后就会将程序的执行权限交给更高级的执行控制对象。
在 Go
语言中,代码包中的源码文件名可以是任意的,这些任意名称的源码文件都必须以包声明语句作为文件中代码的第一行。比如 src
目录下的代码包 common/upload
包中的所有源码文件都要先声明自己属于common
/upload
包:
package upload
package
是 Go
语言中用于包声明语句的关键字。 Go
语言规定包声明中的包名为代码包路径的最后一个元素。如上,common/upload
包的包路径为 common/upload
,而包声明中的包名则为 upload
。
而针对命令源码文件(即包含 main
函数的 .go
文件),无论存放在哪个包中,它都必须声明为属于 main
包。
标准包的源码位于 $GOROOT/src/
下面,标准包可以直接引用。自定义的包和第三方包的源码必须放到 $GOPATH/src/
目录下才能被引用。导入包需要使用关键字 import
,它会告诉编译器你想引用该位置的包内的代码。如果需要导入多个包,习惯上是将 import
语句包装在一个导入块中。
包的引用路径有两种写法, 一种是绝对路径,另一种是相对路径。
要在代码中引用其他包的内容,需要使用 import
关键字导入使用的包。具体语法如下:
import "包的路径"
注意事项:
import
导入语句通常放在源码文件开头包声明语句的下面;GOPATH/src/
后开始计算的,使用/
进行路径分隔。包的导入有两种写法,分别是单行导入和多行导入。
import "包 1 的路径"
import "包 2 的路径"
import (
"包 1 的路径"
"包 2 的路径"
)
包的绝对路径就是 $GOROOT/src
或 $GOPATH/src
后面包的源码的全路径,比如下面的包引用:
import "common/upload"
import "database/sql/driver"
import "database/sql"
upload
包是自定义的包,其源码位于 $GOPATH/src/common/upload
目录下,代码包导入使用的路径就是代码包在工作区的 src
目录下的相对路径,比如 upload
的绝对路径为 /home/wohu/gocode/src/common/upload
,而 /home/wohu/gocode
是被包含在环境变量 GOPATH
中的工作区目录路径,则其代码包导入路径就是common/upload
。
sql
和 driver
包的源码分别位于 $GOROOT/src/database/sql
和 $GOROOT/src/database/sql/driver
下。
编译器会首先查找 Go
的安装目录,然后才会按顺序查找 GOPATH
变量里列出的目录。一旦编译器找到一个满足 import
语句的包,就停止进一步查找。
相对路径只能用于引用 $GOPATH
下的包,标准包的引用只能使用全路径引用。比如下面两个包:
包 a
的路径是 $GOPATH/src/lab/a
,包 b
的源码路径为 $GOPATH/src/lab/b
,假设 b
引用了 a
包,则可以使用相对路径引用方式。示例如下:
// 相对路径引用
import "../a"
// 绝对路径引用
import "lab/a"
常用的包引用有以下 4 种格式,我们以 fmt
包为例进行说明。
import "fmt”
此时可以用 fmt.
作为前缀引用包内可导出元素,这是常用的一种方式。
import F "fmt”
此时相当于给包 fmt
起了个别名 F
,用 F.
代替标准的 fmt.
作为前缀引用 fmt 包内可导出元素。
import . "fmt"
此时相当于把包 fmt
的命名空间直接合并到当前程序的命名空间中,使用 fmt
包内可导出元素可以不用前缀 fmt.
,直接引用。示例如下:
package main
import . "fmt"
func main() {
// 不需要加前级fmt.
Println("hello , world”)
}
init
函数使用标准格式引用包,但是代码中却没有使用包,编译器会报错。如果包中有 init
初始化函数,则通过 import packageName
这种方式引用包,仅执行包的初始化函数,即使包没有 init
初始化函数,也不会引发编译器报错。示例如下:
import _ "fmt"
下划线字符 _
在 Go
语言里称为空白标识符,这个标识符用来抛弃不想继续使用的值,如给导入的包赋予一个空名字,或者忽略函数返回的你不感兴趣的值。
Go
工具链会使用导入路径确定需要获取的代码在网络的什么地方。
import "github.com/net/http"
用导入路径编译程序时, go build
命令会使用 GOPATH
的设置,在磁盘上搜索这个包。
事实上,这个导入路径代表一个 URL
,指向 GitHub
上的代码库。如果路径包含 URL
,可以使用 Go
工具链从 分布式版本控制系统获取包,并把包的源代码保存在 GOPATH
指向的路径里与 URL
匹配的目录里。
这个获取过程使用 go get
命令完成。go get
将获取任意指定的 URL
的包,或者一个已经导入的包所依赖的其它包。由于 go get
的这种递归特性,这个命令会扫描某个包的源码树,获取能找到的所有依赖包。
当导入多个代码包时,需要用圆括号括起它们,且每个代码包名独占一行。在调用被导入代码包中的函数或使用其中的结构体、变量或常量时,需要使用包路径的最后一个元素加 .
的方式指定代码所在的包。
例如,如果我们有两个包 logging
和 go_lib/logging
, 并且有相同的方法 logging_print()
,且有一个源码文件需要导入这两个包(标准引用):
import (
"logging"
"go_lib/logging"
)
则这句代码 logging.logging_print()
就会引起冲突, Go
语言无法知道 logging.
代表的是哪一个包。所以,在 Go
语言中,如果在同一个源码文件中使用上述方法导入多个代码包,那么代码包路径的最后一个元素不可以重复。
如果用这段代码包导入代码,在编译代码时,Go 语言会抛出
”logging redeclared as imported package name”
的错误。如果确实需要导入,当有这类重复时,我们可以给它们起个别名来区别(别名引用):
import (
la "logging"
lb "go_lib/logging"
)
调用包中的代码:
var logger la.Logger = la.logging_print()
这里不必给每个引起冲突的代码包都起一个别名,只要能够区分它们就可以了。
如果我们想直接调用某个依赖包的程序,就可以用 .
来代替别名(省略引用)。
import (
. "logging"
lb "go_lib/logging"
)
在当前源码文件中,可以直接进行代码调用了:
var logger Logger = logging_print()
Go
语言把变量、常量、函数、结构体和接口统称为程序实体,而把它们的名字统称为标识符。标识符可以是任何 Unicode
编码可以表示的字母字符、数字以及下划线 ”_”,并且,首字母不能是数字。标识符的首字母的大小写控制着对应程序实体的访问权限。
如果标识符的首字母是大写的,那么它对应的程序实体就可以被本代码包之外的代码访问到,也可以称其为可导出的。否则对应的程序实体就只能被本包内的代码访问。当然,还需要有以下两个额外条件:
GOPATH
中的工作区目录中。如果代码包 logging
中有一个叫做 getSimpleLogger
的函数,那么光从这个函数的名字上我们就可以看出,这个函数是不能被包外代码调用的。
如果我们只想初始化某个代码包而不需要在当前源码文件中使用那个代码包中的任何代码,即可以用 _
来代替别名(仅执行包初始化 init 函数的引用方式)。
import (
_ "logging"
)
init
函数,包加载会执行全部的 init
函数,但并不能保证执行顺序,所以不建议在一个包中放入多个 init
函数,将需要初始化的逻辑放到一个 init
函数里面。Go
编译器保证 d 的 init
函数只会执行一次。在 Go
语言中,可以有专门的函数负责代码包初始化。这个函数需要无参数声明和结果声明,且名称必须为 init
,如下:
func init() {
println("Initialize")
}
Go
语言会在程序真正执行前对整个程序的依赖进行分析,并初始化相关的代码包。也就是说,所有的代码包初始化函数都会在 main
函数(命令源码文件中的入口函数)之前执行完成,而且只会执行一次。并且,当前代码包中的所有全局变量的初始化都会在代码包初始化函数执行前完成。这就避免了在代码包初始化函数对某个变量进行赋值之后又被该变量声明中赋予的值覆盖掉的问题。
每个包可以包含任意多个 init
函数,这些函数都会在程序执行开始的时候被调用。所有被编译器发现的 init
函数都会安排在 main
函数之前执行。 init
函数用在设置包、初始化变量或者其他要在程序运行前优先完成的引导工作。
Go
里面有两个保留的函数: init
函数(能够应用于所有的 package
)和 main
函数(只能应用于 package main
)。这两个函数在定义时不能有任何的参数和返回值。
虽然一个 package
里面可以写任意多个 init
函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个 package
中每个文件只写一个 init
函数。
Go
程序会自动调用 init()
和 main()
,所以不需要在任何地方调用这两个函数。每个 package
中的 init
函数都是可选的,但 package main
只能包含一个 main
函数。
程序的初始化和执行都起始于 main
包。如果 main
包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到 fmt
包,但它只会被导入一次,因为没有必要导入多次)。
当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行 init
函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对 main
包中的包级常量和变量进行初始化,然后执行 main
包中的 init
函数(如果存在的话),最后执行 main
函数。下图详细地解释了整个执行过程:
见 go 语言基础 《main函数和init函数》 78 页说明
init
函数特征总结:
所以简而言之,你只需要记住这三点就可以了:
init
函数按出现次序进行自动调用;(有的说法是编译器不能保证调用顺序,待确认?)重点关注 init
函数具备的几种行为特征:
init
函数在整个 Go
程序生命周期内仅会被执行一次;init
函数是顺序执行的,只有当一个 init
函数执行完毕后,才会去执行下一个 init
函数。这里举出《Go并发编程实战》中的例子,帮助理解上面的包初始化,如下:
package main // 命令源码文件必须在这里声明自己属于main包 import ( // 引入了代码包fmt和runtime "fmt" "runtime" ) func init() { // 包初始化函数 fmt.Printf("Map: %v\n", m) // 先格式化再打印 // 通过调用runtime包的代码获取当前机器所运行的操作系统以及计算架构 // 而后通过fmt包的Sprintf方法进行字符串格式化并赋值给变量info info = fmt.Sprintf("OS: %s, Arch: %s", runtime.GOOS, runtime.GOARCH) } // 非局部变量,map类型,且已初始化 var m map[int]string = map[int]string{1: "A", 2: "B", 3: "C"} var info string // 非局部变量,string类型,未被初始化 func main() { // 命令源码文件必须有的入口函数 fmt.Println(info) // 打印变量info }
输出
Map: map[1:A 2:B 3:C]
OS: windows, Arch: amd64
在同一个代码包中,可以存在多个代码包初始化函数,甚至代码包内的每一个源码文件都可以定义多个代码包初始化函数。
Go
语言编译器不能保证同一个代码包中的多个代码包初始化函数的执行顺序。如果要求按特定顺序执行的话,可以考虑使用 Channel
。
当我们修改了一个源文件,我们必须重新编译该源文件对应的包和所有依赖该包的其他包。
即使是从头构建, Go
语言编译器的编译速度也明显快于其它编译语言。 Go
语言的闪电般的编译速度主要得益于三个语言特性。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。