本文为译文。原文地址:package
什么是包?为什么他们会被使用
在我们的系列课程中,我们已经看见了go的程序。但是只有一个文件,里面包含一个main函数和一组其他的函数。在实际开发场景中不会把所有的源代码都写在一个文件里。这样即不利于重用也不好维护。所以我们引入了包的概念。
包用来管理go的源代码使其更具有重用性和可读性。包是在相同目录内的go的源文件的集合。包提供了代码分割,因此项目的维护才能变的简单。
比如我们要写一个金融项目。一些功能是单利计算,一些功能是复利计算,还有一些借贷计算。一种简单的组织应用的形式就是通过功能。我们可以创建三个包simpleinsterest``compoundinterest``load。如果loan包需要进行“单利”计算。那么它可以简单的引入simpleinsterest里的功能。这种形式就是代码复用了。我们将通过创建一个简单的应用来学习package。
main function and main package
任何一个可执行的go应用都必须包含一个主函数。这是应用可执行的入口。主函数一般应该在主包内。
package packagename说明这个源文件属于哪个包。这个应该在每个源文件的第一行做声明。
让我们开始为我们的应用创建一个主函数和主包。首先我们先创建一个目录
mkdir ~/Documents/learnpackage/
在我们的learnpackage下创建一个main.go文件。文件内容如下
package main
import "fmt"
func main() {
fmt.Println("Simple interest calculation")
}
package main说明main.go文件属于main包。import "packagename"声明导入一个已经存在的包。packagename.FunctionName是调用包内函数的语法。
在第三行我们导入了一个fmt包使用了其Println方法。fmt是go的一个标准包,其被内置在go的标准库的一部分。然后这边有一个主函数打印了一串简单的字符串。
我们移动到learnpackage目录下,然后进行文件编译。输入go install。如果一切顺利,我们的二进制包就生成了。直接在终端运行learnpackage就会输出对应的字符串了。
如果你不理解go install命令或者报错了。可以参考我之前的文章 hello world
go module
我们会把“单利”相关的功能都放在simpleinterest包。我们需要一个simpleinterest目录,这个目录里面会包含和‘单利‘相关的种种能力。在我们创建包之前,我们需要先理解go modules。因为go modules需要被创建一个自定义的包。
go的模块可以理解为包的集合。现在你可能会想,为什么我们需要一个go module来创建一个自定义的package.答案是,我们创建的自定义的package的导入路径是基于go module衍生出来的。除了这个,所有的第三包(比如github上的源码)。当我们创建一个新的模块时,会同时创建一个go.mod文件,这个文件会包含以来的包名及其版本。
创建一个go module
首先我们先移动到我们的目录下cd ~/Documents/learnpackage/在命令函输入下面的命令,创建一个名叫learnpackage的模块。上面的命令会创建一个go.mod的文件。其内容如下
module learnpackage
go 1.13
第一行说明了模块名是啥。就如我们之前提到的,learnpackage是我们在这个模块导入其他包的基础路径。第三行说明了go的版本是1.13
创建一个simpleinterest包
在go中我们一般约定包名和文件夹名称一致。和Java差不多。
我们现在在learnpackage目录下创建一个simpleinterest目录。所有在simpleinterest目录下的文件都可以认为是这个包的。所以我们有了如下的目录结构
├── learnpackage
│ ├── go.mod
│ ├── main.go
│ └── simpleinterest
│ └── simpleinterest.go
添加下面的代码到 simpleinterest文件
package simpleinterest
//Calculate calculates and returns the simple interest for a principal p, rate of interest r for time duration t years
func Calculate(p float64, r float64, t float64) float64 {
interest := p * (r / 100) * t
return interest
}
导入一个自定义的包
去使用一个自定义的包,我们必须先导入它。导入路径是模块名加包的子路径。在我们的例子中模块名是learnpackage包是直接在learnpackage下的simpleinterest目录。
所以import "learnpackage/simpleinterest"将导入simpleinterest包。
如果我们有如下的目录结构
learnpackage
│ └── finance
│ └── simpleinterest
然后导入声明就该是这样的import "learnpackage/finance/simpleinterest"添加下面的代码在main.go
package main
import (
"fmt"
"learnpackage/simpleinterest"
)
func main() {
fmt.Println("Simple interest calculation")
p := 5000.0
r := 10.0
t := 1.0
si := simpleinterest.Calculate(p, r, t)
fmt.Println("Simple interest is", si)
}
关于go install 多一点的知识
现在我们理解了包是如何工作的,是时候应该再讨论一下go install了。go的工具比如go install,工作在现在目录的上下文中。让我们理解其意义,到现在,我们在~/Documents/learnpackage/目录下运行go install如果我们在其他目录下运行go install就会失败。
如果cd ~/Documents/然后go install learnpackage
can't load package: package learnpackage: cannot find package "learnpackage" in any of:
/usr/local/Cellar/go/1.13.7/libexec/src/learnpackage (from $GOROOT)
/Users/nramanathan/go/src/learnpackage (from $GOPATH)
让我们来理解一下这后边的原因。go install有一个可选的包名参数(我们的例子中就是learnpackage).然后它视图编译主函数,如果包存在在当前目录,如果不存在就一直往其父目录查找。
我们现在在Document目录,所以没go.mod,然后就报错了。
但是要是我们使用go install没有指定包名呢?如果没指定包名,go install就会默认在当前目录下找。这就是为啥我们在~/Documents/learnpackage里这样也工作OK。所以下面三个命令在~/Documents/learnpackage目录里是等价的
go install
go install .
go install learnpackage
刚才我也提到了,go install也会往父目录一层一层查。我们来检测一下
cd ~/Documents/learnpackage/simpleinterest/
go install learnpackage
go install将成功的在父目录找到learnpackge.所以也不会报错😁
导出名称
如果想在一个包里访问另一个包的函数或者变量,那么就必须大写。
init function
go里的每个包都可以有一个init函数。init函数不需要返回值,也不要任何参数。init函数不能在我们的代码里显示调用。当包被初始化的时候,它会被自动调用。语法如下
func init() {
}
初始化函数可以被用来执行初始化任务、在程序启动之前,校验程序的正确性。
包的初始化顺序如下
- 包级别的变量被初始化
- 调用初始化函数。一个包可以有多个初始化函数(无论是在单文件还是被分发在多个文件中),他们将被按照提供给编译器的顺序调用。
如果一个包引入另一个包。被引入的包的初始化函数优先执行。
如果一个包被多个包引入, 那么这个包的初始化函数也只会执行一次。
让我们修改一下我们的文件,以便更好的理解init函数。
package simpleinterest
import "fmt"
/*
* init function added
*/
func init() {
fmt.Println("Simple interest package initialized")
}
//Calculate calculates and returns the simple interest for principal p, rate of interest r for time duration t years
func Calculate(p float64, r float64, t float64) float64 {
interest := p * (r / 100) * t
return interest
}
package main
import (
"fmt"
"learnpackage/simpleinterest" //importing custom package
"log"
)
var p, r, t = 5000.0, 10.0, 1.0
/*
* init function to check if p, r and t are greater than zero
*/
func init() {
println("Main package initialized")
if p < 0 {
log.Fatal("Principal is less than zero")
}
if r < 0 {
log.Fatal("Rate of interest is less than zero")
}
if t < 0 {
log.Fatal("Duration is less than zero")
}
}
func main() {
fmt.Println("Simple interest calculation")
si := simpleinterest.Calculate(p, r, t)
fmt.Println("Simple interest is", si)
}
整个程序的初始化顺序如下:
- 被导入的包初始化。因此首先
simpleinterest包被初始化。所以他的init函数被调用。 - 包级变量p、r、t被初始化。
main.go的初始化函数被调用。- 最后
main function被调用。
所以整个程序的输出如下
Simple interest package initialized
Main package initialized
Simple interest calculation
Simple interest is 500
如果我们修改一下代码,将变量p改成-5000.0。再运行程序,你会看到如下结果。(log.Fatal()会中断程序的执行)
Simple interest package initialized
Main package initialized
2020/02/15 21:25:12 Principal is less than zero
使用空白标识符
在go里如果导入一个包,但是没有使用它,这是语法上的错误。这个的原因是无用的包会造成包的编译时间显著上升。但是开发过程中这个还是比较常见的,避免的方式就是加个_空白标识符。
package main
import (
"learnpackage/simpleinterest"
)
var _ = simpleinterest.Calculate
func main() {
}
有时候我们导入一个包,只需要其执行其初始化函数,但不会调用其任何方法。只要按如下语法写就OK了。
package main
import (
_ "learnpackage/simpleinterest"
)
func main() {
}