go——工程结构
Go是一门推崇软件工程理念的编程语言,它为开发周期的每个环节都提供了完备的工具和支持。
Go语言高度强调代码和项目的规范和统一,这几种体现在工程结构或者说代码体制的细节之处。
1.工作区
一般情况下,Go源码文件必须放在工作区中。但是对于命令源码文件来说,这不是必须的。
工作区其实就是一个对应于特定工程的目录,它应该包含3个子目录:src目录、pkg目录和bin目录。
(1)src目录
用于以代码包的形式组织并保存Go源码文件,这里的代码包与src下的子目录一一对应。
例如,若一个源码文件被声明属于代码包log,那么它就应该保存在src/log目录下。
当然,你也可以直接把Go源码文件直接放在src目录下,但这样的Go源码文件就只能被声明属于main代码包。
除非用于临时测试或演示,一般还是建议把Go源码文件放入特定的代码包中。
(2)pkg目录
用于存放通过go install命令安装后的代码包的归档文件。
前提是代码包中必须包含Go库源码文件。归档文件是指那些名称以“.a”结尾的文件。
该目录与GOROOT目录下的pkg目录功能类似。
区别在于,工作区中的pkg目录专门用来存放用户代码的归档文件。
编译和安装用户代码的过程一般会以代码包为单位进行。
比如log包被编译安装后,将生成一个名为log.a的归档文件,并存放在当前工作区的pkg目录下的平台相关目录中。
(3)bin目录
与pkg目录类似,在通过go install命令完成安装后,保存由Go命令源码文件生成的可执行文件。
(其实上面文字的意思就是什么类型的文件放在什么目录下面,尽量规划好工作区)
命令源码文件:就是声明属于main代码包并包含无参声明和结果声明的main函数的源码文件。
这类源码文件就是程序的入口,它们可以独立运行(使用go run命令),
也可以通过go build或go install命令得到相应的可执行文件。
库源码文件:是指存在于某个代码包中普通源码文件。
2.源码文件
(1)命令源码文件
如果一个源码文件被声明属于main代码包,且该文件代码中包含无参数声明和结果声明的main函数,则它就是命令源码文件。
命令源码文件可以直接通过go run命令直接启动运行。
同一个代码中的所有的源码文件,其所属代码包的名称必须一致。
如果命令源码文件和库源码文件处于同一个代码包中,那么在该包中就无法正确执行go build和go install命令。
换句话说,这些源码文件将无法通过常规方法编译和安装。
因此,命令源码文件通常会单独放在一个代码包中,因为通常一个程序模块或软件的启动入口只有一个。
同一个代码包中可以有多个命令源码文件,可通过go run命令分别运行,但这会使go build和go install命令无法编译和安装该代码包。
当代码包中有且只有一个命令源码文件时,在文件所在目录中执行go build命令,
则可在该目录下生成一个与目录同名的可执行文件;
若使用go install命令,则可在当前工作区的bin目录下生成相应的可执行文件。
需要注意的是,只有当前环境变量GOPATH中只包含一个工作区的目录路径时,
go install命令才会把命令源码文件安装到当前工作区的bin目录下;
(2)库源码文件
通常,库源码文件声明的包名会与它直接所属的代码包一致,且库源码文件中不包含无参数声明和无结果声明的main函数。
通过执行go install命令,成功安装了该包并生成了若干归档文件。
由于我们指定了工作区GOPATH的路径是/root/example.v2,则会在这个目录下生成bin和pkg文件。
run install之前:
run install之后:
(3)测试源码文件
测试源码文件是一种特殊的库文件,可以通过执行go test命令运行当前代码包下的所有测试源码文件。
成为测试源码文件的充分条件有两个:
A.文件名需要以"_test.go"结尾
B.文件中需要至少包含一个名称以Test开头或Benchmark开头,且拥有一个类型为*testing.T或*testing.B的参数的函数。
*testing.T或*testing.B是两个结构体类型。而*testing.T或*testing.B则分别为前两者的指针类型。它们分别是功能测试和基准测试所需。
当在一个代码包中之心go test命令时,该代码包中的所有测试源码文件会被找到并运行。
3.代码包
在Go中,代码包是代码编译和安装的基本单元,也是非常直观的代码组织形式。
(1)包声明
在Go语言中,代码包中的源码文件可以任意命令。
但是,这些任意名称的源码文件都必须以包声明语句作为文件代码中的第一行。
例如,example.v2/src/gopcp.v2/helper/log/base包中的所有源码文件都先声明自己属于某一个代码包:
分别打印第一行:
其中,package是Go中用于包声明语句的关键字。
Go规定包声明中的包名是代码路径的最后一个元素。
所以包名是base。
但是,不论命令源文件存放在哪个代码包中,它都必须声明属于main包。
(2)包导入
代码包gopcp.v2/helper/log中的logger.go需要依赖base子包和logrus子包,
因此需要在源码文件中使用代码包导入语句,如:
import "gopcp.v2/helper/log/base" import "gopcp.v2/helper/log/logrus"
全路径是:/root/example.v2/src/gopcp.v2/helper/log,但是所有的源代码都在src下面,所以这里使用相对路径。
当导入多个代码包时,你需要用圆括号,每个代码包名占一行。
import ( "gopcp.v2/helper/log/base" "gopcp.v2/helper/log/logrus" )
同一个源码文件中导入多个代码包的最后一个元素不能重复,否则一旦使用其中的程序实体,就会引起编译错误。
但是,如果你只导入不适用,同样会引起编译错误,一个解决办法就是为其中一个起别名。
import ( "gopcp.v2/helper/log/logrus" mylogrus "gopcp.v2/helper/log/logrus" )
如果我们想不加前缀而直接使用某个依赖包中的程序实体,就可以用"."来代替别名。
import ( . "gopcp.v2/helper/log/logrus" )
所以我们就可以这样引用:
var logger = NewLogger("gopcp") //NewLogger是logrus中的一个函数。
我们可以看到,因为"."的缘故,可以直接引用包中的函数,而不需要指定路径。
在Go中,变量、常量、函数和类型声明可统称为程序实体,而它们的名称统称为标识符。
标识符可以是Unicode字符集中任意能表示自然语言文字的字符、数字以及下划线(_)。标识符不能以数字或下划线开头。
实际上,标识符的首字符的大小写控制着对应程序实体的访问权限。
如果标识符的首字符是大写形式,那么它所对应的程序实体就可以被本代码之外的代码访问到,也称之为可导出或公开的;
否则,对应的程序实体就只能被本包内的代码访问,也成为不可导出的或包私有的。
要想成为可导出的程序实体,还需要额外满足以下两个条件。
程序实体必须是非局部的。局部的程序实体是指:它被定义在了函数或结构体的内部。
代码包所属目录必须包含在GOPATH中定义的工作区目录中。
代码包导入还有另一种情况:如果只想初始化某个代码包,
而不需要在当前源码文件中使用那个代码包中的任何程序实体,就可以用“_”来代替别名。
import ( _ "gopcp.v2/helper/log/logrus" )
这种情况下,我们只是触发了这个代码包的初始化操作,符号"_"就像一个垃圾桶。
(3)包初始化
在Go中,可以有专门的函数负责代码包初始化,成为代码包初始化函数。
这个函数无任何参数声明和结果声明,且名称必须为init。
func init() { fmt.PrintLn("Initialize...") }
Go会在程序真正执行前对整个程序的依赖进行分析,并初始化相关的代码包。
也就是说,所有的代码包初始化函数都会在main函数(命令源码文件的入口函数)执行前执行完毕,而且只会执行一次。
另外,对于每一个代码包来说,其中的所有全局变量的初始化,都会在代码包的初始化函数执行前完成。
这避免了在代码包初始化函数对某个变量进行赋值之后,又被该变量声明中赋予的值覆盖掉的问题
下面是一个简单示例:
//如果一个源码文件被声明属于main代码包,且该文件代码包含无参数声明和结果声明的main函数,则它就是命令源码文件。 package main //命令源码文件必须在这里声明自己属于main包 import ( //导入标准库代码包fmt和runtime "fmt" "runtime" ) func init() { //代码初始化函数 fmt.Printf("Map: %v\n", m) //这里可以直接获取到变量m,可以看出变量的初始化在代码包初始化之前完成 //通过runtime获取当前操作系统和计算架构 //通过fmt的Sprintf进行格式化然后赋值给info info = fmt.Sprintf("OS:%s,Arch:%s", runtime.GOOS, runtime.GOARCH) } //非局部变量,map类型,且已初始化,因为已经赋值 var m = map[int]string{1: "A", 2: "B", 3: "C"} //非局部变量,string类型,未初始化,因为还未赋值 var info string func main() { //命令源码文件必须有入口函数,也可以叫做主函数 fmt.Println(info) //代码包的初始化会在main函数执行前执行完毕 } //同一个代码包中可以存在多个代码初始化函数,甚至代码包内的每一个源码文件都可以定义多个代码初始化函数。 //Go不会保证同一个代码包中多个代码包初始化函数的执行顺序。 //被导入的代码吧的初始化函数总是会先执行,例如上面fmt和runtime中如果有init函数,那么就会先执行。