第七篇 包

欢迎来到Golang教程系列的第七篇

什么是包和为什么使用包

到目前为止,我们写的Go程序只有一个文件,里面包括一个main函数,以及其他几个函数。在真实的场景中,把所有的源码都放在单个文件上的这种方式是不可拓展的,这种方式也会导致代码不可复用和维护。关于这个问题,包package能帮助我们解决。

包可以用来组织Go源码,以便于更好的拓展性和可读性。包是放在同一个目录下的Go源码文件。包提供了代码区分,因此可以更好地维护Go项目。

例如,我们要用Go写一个金融应用,带有一些功能,单利计算,复利计算和贷款计算。有一个简单的方式是可以通过功能性来组织项目。我们可以创建几个包simpleinterestcompoundinterestload。如果loan包需要单利计算,它就能简单地导入simpleinterest包,这样就可以实现代码复用。

我们将会通过创建一个简单的应用说明,给出本金、利率和时间来计算单利。

main函数和main包

每个可执行的Go应用都必须包含main函数,这个函数是执行的入口,而且main函数应该放在main包里面。

package packagename指定属于package包的特定源文件,这应该放在每个源文件的第一行。

接下来,我们开始为我们的应用创建main函数和main包。

执行下面的命令在用户的Documents目录下新建一个命名为learnpackage的目录。

mkdir ~/Documents/learnpackage

learnpackage目录下新建一个main.go文件,文件包括下面的内容。

package main

import "fmt"

func main() {

	fmt.Println("simple interest calcilation")
}

package main这行代码指明了这个文件是属于main包的,而import "packagename"语句则是用来引入一个已经存在的包。packagename.FunctionName()则是调用一个包里的一个函数。

语句import "fmt",我们引入fmt包,是为了使用PrintIn函数,fmt是一个是内置在Go标准库的一个标准包。以上,就是一个打印了simple interest calcilation的main函数。

进入learnpackage目录,并编译程序

cd ~/Documents/learnpackage/  

然后输入下面的命令

go install

如果一切正常,我们的二进制文件已编译完成,且等待执行。在命令行输入命令learnpackage,然后会看到下面的输出。

simple interest calcilation

如果你不明白go install是怎么运作的,或者运行报错了,可以回看第二篇 HelloWorld有说明。

Go模块

我们组织代码的方式就是所有有关单利的功能点都放在simpleinterest包里面。所以,我们需要新建一个包含计算单利的函数的自定义包simpleinterest。在新建自定义包之前,因为Go Modules需要创建一个自定义包,我们需要先明白什么是Go Modules模块,。

GO Modules是Go Packages的集合,或许你就有疑问了,为什么我们需要Go Modules去创建一个自定义的包呢?答案就是我们自定义包的引入路径就是来自于go module的名字。除此之外,所有其它的第三方包(例如一些来自github的源码),我们用到的都会跟带着版本出现在go.mod文件里。在下个章节,你将会更明白些。

另外一个问题也可能浮现在我们脑海,为什么我们到现在还不创建一个Go Module呢?答案就是,到目前为止,我们还没用到我们的自定义包。

理论说的足够多了。让我们开始创建我们的go module和custom package.

创建Go模块

输入cd ~/Documents/learnpackage以确保你在learnpackage目录里面。在这个目录里面,执行下面的命令来创建一个命名为learnpackage 的go模块.

go mod init learnpackage

上面的命令将会创建一个go.mod的文件,下面是文件的内容。

module learnpackage

go 1.17

module learnpackage指明了模块的名称是learnpackage,正如我们先前提及,learnpackage在这个模块引进其它包的基本路径。go 1.17指明了这个模块文件用的go版本是1.17

创建一个关于单利的自定义包

包的一些源文件应该分开放置在他们不同的文件夹里,Go语言习惯上文件夹名称和包名保持一致

让我们在learnpackage目录下通过mkdir simpleinterest创建一个文件夹simpleinterest

所有在simpleinterest文件夹里面的文件都要以package simpleinterest开头,因为它们都属于simpleinterest包。

simpleinterest文件夹里创建一个文件simpleinterest.go

下面就是我们应用的目录结构。

├── learnpackage
│   ├── go.mod
│   ├── main.go
│   └── simpleinterest
│       └── simpleinterest.go

simpleinterest文件新增下面的代码。

func Calculate(p float64, r float64, t float64) float64 {
    interest := p * (r / 100) * t
    return interest
}

在上面的代码中,我们创建了一个计算并且返回单利的函数Calculate,这个函数是不言而喻的,它计算并且返回了单利。

注意,函数名称Calculate是以大写开头的,这是必须的,我们下面会解释为什么要这样做。

导入自定义包

要是用自定义包,我们首先需要引入它。引入路径就是模块的名称加上其子目录下的包名。在我们的例子中,module名称是learnpackage,包simpleinterest是放在learnpackage目录下的simpleinterest文件夹里。

├── 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)
}

上面的代码中引入了simpleinterest包,并且使用了Calculate函数获得单利值。标准库里面的包不需要加上模块名称前缀,因此"fmt"不用模块前缀也可以正常运行。上面的程序运行有以下输出。

simple interest calculation
simple interest is  500

更多关于安装的信息

现在我们已经理解了包如何工作,是时候讲多一些关于go install的内容。像go install这样的Go工具工作在当前目录的上下文中。这是什么意思呢?到目前为止,我们已经在~/Documents/learnpackage/目录下执行了go install。但如果我们在其它目录下执行就会失败。

你可以尝试进入cd ~/Documents,然后运行go install learnpackage,它将会失败,并报错

cannot find package "learnpackage" in any of:
	/usr/local/go/src/learnpackage (from $GOROOT)
	/Users/swon/go/src/learnpackage (from $GOPATH)

在这个错误之后,我们理应明白,go install接收包名作为可选参数(在我们例子里,包名是learnpackage),如果包在当前执行目录,或者在父级目录等等,那么它就开始编译main函数。

我们当前是在Document目录下,这没有go.mod文件,因此,go install解释后找不到learnpackage包。

我们进入~/Documents/learnpackage/目录下,这有一个go.mod文件,在go.mod文件中,我们module名称就是learnpackage

所以,go install learnpackage~/Documents/learnpackage目录下就是执行正常。

但是,到目前为止,我们只是用了go install命令,并没有指明包名。如果不指定包名的话,go install将会取当前工作目录下的module名称。这就是为什么当go install~/Documents/learnpackage目录下没有带包名也正常运行。所以下面三个命令在目录~/Documents/learnpackage下运行是等价的。

go install

go install .

go install learnpackage

我刚才也提到了go install也有能力去父级目录递归查找go.mod文件。让我们看下它怎么工作的。

cd ~/Documents/learnpackage/simpleinterest/

上面的命令会让我们进入simpleinterest目录,在这个目录里,我们执行

go install learnpackage

Go install成功在父目录learnpackage下找到定义了learnpackage的文件go.mod,因此,它是有效的。

输出名称?

我们在Simple Interest包中Calculate函数名称是大写开头的,这在Go中有着特别的意义,在Go中以大写字母开头一些变量或函数都是对外输出的名字,只有对外输出的变量和函数可以被其他包访问。在我们的例子中,我们想要访问main包的Calculate函数,因此,这个是大写的。

如果修改simpleinterest.go文件中的函数Calculatecalculate,然后,我们尝试在main.go中使用simpleinterest.calculate(p, r, t)调用函数时,编译就会报错。

 cannot refer to unexported name simpleinterest.calculate

所以,你想要从外部访问一个包的函数,那么它的首字母应该要大写的。

初始化函数

在Go中每个包都包含了一个初始化init函数,init函数没有返回值,也没有参数。init函数在源码中不能显式调用。当包初始化时,它会自动调用。init函数有下面的语法。

func init() {
}

init函数可以执行初始化任务,也可以用来在执行程序执行前验证程序的正确性。

下面是包的初始化顺序:

  1. 包级别的变量首先初始化
  2. 接着就调用init函数,一个包可以有多个init函数(在一个文件或分布在多个文件),它们是按照编译的顺序依次调用。

如果一个包引起其他包,那么引入的包首先初始化。

一个包即使是被多个包引入,都只会初始化一次。

下面让我们改变一下我们的应用以便于更好地理解init函数。

首先就从在simpleinterest.go文件添加init函数开始。

package simpleinterest

import "fmt"

/*
 * 添加初始化函数
 */
func init() {
	fmt.Println("simple interest package initialize")
}

func calculate(p float64, r float64, t float64) float64 {
    interest := p * (r / 100) * t
    return interest
}

我们添加一个简单的初始化函数,就输出个字符串Simple interest package initialised

现在我们修改main包,我们知道本金、利率、时间在计算的时候都要大于0,我们将在main.go文件定义包级别变量和用init函数来检查。

下面是是修改过后的main.go文件

package main


import (
	"fmt"
	"learnpackage/simpleinterest"
	"log"
)

var p, r, t = 5000.0, 10.0, 1.0

/*
 * 初始化函数
 */
func init() {

	println("Main package initialized")
	if p < 0 {
		log.Fatal("principal is lee 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)
}

下面是main.go改变的地方

  1. p,r and t变量从main函数级别转移到包级别变量
  2. 添加了一个init函数,这个init函数打印日志,而且当本金、利率、时间小于0时,使用log.Fatal函数终止程序执行。

下面是程序的初始化顺序

  1. 导入的包首先初始化,因此simpleinterest包首先初始化,然后调用它的初始化方法。
  2. 包级别的变量p,rt接着初始化
  3. main的init函数被调用
  4. main函数最后被调用

如果你执行程序,将有下面的输出

simple interest package initialize
Main package initialized
simple interest calculation
simple interest is  500

就像预期那样,simpleinterest包的Init函数首先被调用,接着是厨师换包级别变量p,tr。接着是main包的init函数被调用,它检查了p,t,r是否小于0,当条件为真时,就终止程序。(我们将在下一篇教程学习if语句),现在,你可以假设if p < 0会判断P是否小于0,如果是,那么程序就会终止。我们也给t,r写了相似的语句,在这个例子中,所有的这些条件都为假时,程序继续运行。最后,调用main函数。

让我们改变一点程序学习下init函数

var p, r, t = 5000.0, 10.0, 1.0

改成

var p, r, t = -5000.0, 10.0, 1.0

我们把p初始化为负数

现在你再运行程序,你就会看到

simple interest package initialize
Main package initialized
2022/01/03 16:42:47 principal is lee than zero
exit status 1

p是负数,因此,当init函数运行,打印完principal is lee than zero,程序就会终止。

使用空白标识符

如果在Go中引入了一个包在代码中却没有使用它,是非法的。如果你这么做,编译器会报错。这么做的原因是避免编译没用到的包时浪费大量的编译时间。
用下面的代码替换main.go中的。

package main

import (
    "learnpackage/simpleinterest"
)

func main() {

}

上面的程序将会报错

# learnpackage
./main.go:4:2: imported and not used: "learnpackage/simpleinterest"

但是有些包是非常常用的,只是现在还没有用上,就可以使用_空白标识符保留它们。

用下面的代码可以隐藏上面程序的错误。

package main

import (
    "learnpackage/simpleinterest"
)
var _ = simpleinterest.Calculate
func main() {

}

var _ = simpleinterest.Calculate可以消除了错误。我们应该跟中这类错误,直到应用开发完成功后确认有没有用到这些包,没有的话,则移除它们。因此,我们建议在引入包之后,在包级别写上error silencers(错误消音器?)

有时我们需要引入包以确保初始化正常发生,即使我们不需要用到包的任何函数或变量。例如,我们可能想要确认simpleinterest包的init函数有被调用,但是并没有我们代码中使用使用这个包。_空白标识符就可以派上用场了。

package main

import (
    _ "learnpackage/simpleinterest"
) 

func main() {

}

运行上面的的程序,将会输出simple interest package initialize,我们就成功初始化了simpleinterest包,即使我们没有在代码中用到这个包。

以上就是本篇的全部,感谢阅读,这是我第一次翻译,难免会有翻译不当的地方,如果有什么反馈和评论,欢迎提出来!

原文地址: https://golangbot.com/go-packages/