7、package

73 阅读8分钟

本文为译文。原文地址: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() {  
}

初始化函数可以被用来执行初始化任务、在程序启动之前,校验程序的正确性。

包的初始化顺序如下

  1. 包级别的变量被初始化
  2. 调用初始化函数。一个包可以有多个初始化函数(无论是在单文件还是被分发在多个文件中),他们将被按照提供给编译器的顺序调用。

如果一个包引入另一个包。被引入的包的初始化函数优先执行。

如果一个包被多个包引入, 那么这个包的初始化函数也只会执行一次。

让我们修改一下我们的文件,以便更好的理解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)
}

整个程序的初始化顺序如下:

  1. 被导入的包初始化。因此首先simpleinterest包被初始化。所以他的init函数被调用。
  2. 包级变量p、r、t被初始化。
  3. main.go的初始化函数被调用。
  4. 最后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() {

}