在GO中通过创建一个简单的应用程序来学习包的教程

62 阅读12分钟

什么是包,为什么要使用它们?

到目前为止,我们看到的Go程序只有一个文件,其中有一个主函数和几个其他函数。在现实世界中,这种将所有源代码写在一个文件中的方法是不可扩展的。这样写出来的代码就不可能被重用和维护。这就是包的作用。

包用于组织 Go 源代码,以提高重用性和可读性。包是驻扎在同一目录下的Go源代码的集合。包提供了代码的分隔,因此,维护Go项目变得很容易。

例如,假设我们正在用Go编写一个金融应用程序,其中的一些功能是简单的利息计算、复利计算和贷款计算。组织这个应用程序的一个简单方法就是按照功能来组织。我们可以创建包simpleinterest,compoundinterestloan 。如果loan 包需要计算简单利息,它可以简单地通过导入simpleinterest 包来实现。这样一来,代码就被重复使用了。

我们将通过创建一个简单的应用程序来学习包,以确定给定本金、利率和以年为单位的时间长度的单利。

主函数和主包

每个可执行的Go程序都必须包含主函数。这个函数是执行的入口。主函数应该位于main包中。

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

让我们开始为我们的应用程序创建主函数和主包。

运行下面的命令,在当前用户的Documents 目录内创建一个名为learnpackage 的目录。

mkdir ~/Documents/learnpackage/  

在我们的learnpackage 目录下创建一个名为main.go 的文件,内容如下。

package main 

import "fmt"

func main() {  
    fmt.Println("Simple interest calculation")
}

这行代码package main ,指定这个文件属于主包。import "packagename" 语句用于导入一个现有的包。packagename.FunctionName() 是调用一个包中的函数的语法。

在第3行,我们导入了3,我们导入fmt 包,以使用Println 函数。fmt 是一个标准包,作为Go标准库的一部分,可以内置。然后是主函数,它打印出Simple interest calculation

编译上述程序的方法是:使用learnpackage 目录,并输入以下命令。

cd ~/Documents/learnpackage/  

并键入以下命令

go install  

如果一切顺利,我们的二进制文件将被编译并准备好执行。在终端输入命令learnpackage ,你将看到以下输出。

Simple interest calculation  

如果你不了解go install 的工作原理,或者如果你遇到错误

go install: no install location for directory /home/naveen/Documents/learnpackage outside GOPATH  
For more details see: 'go help gopath'  

请访问golangbot.com/hello-world…以了解更多。

Go模块

我们将以这样的方式组织代码,即所有与简单利息有关的功能都在simpleinterest 包中。要做到这一点,我们需要创建一个自定义包simpleinterest ,其中将包含计算单利的功能。在创建自定义包之前,我们需要先了解**Go模块,因为创建自定义包需要用到Go模块**。

Go模块只不过是Go包的一个集合。现在你可能会想到这个问题。为什么我们需要Go模块来创建一个自定义包?答案是我们创建的自定义包的导入路径是由Go模块的名称衍生出来的。除此之外,我们的应用程序所使用的所有其他第三方包(例如来自github的源代码)都会和版本一起出现在go.mod 文件中。这个go.mod 文件是在我们创建一个新模块时创建的。你将在下一节中更好地理解这一点。

另一个问题可能会在我们脑海中出现。为什么我们到现在还没有创建一个Go模块?答案是,在本教程系列中,我们直到现在都没有创建自己的自定义包,因此不需要Go模块。

理论讲够了:)。让我们开始行动,创建我们的Go模块和自定义包。

创建一个Go模块

通过输入cd ~/Documents/learnpackage/ ,确保你在目录learnpackage 内。在这个目录下运行以下命令,创建一个名为learnpackage的Go模块。

go mod init learnpackage  

上述命令将创建一个名为go.mod 的文件。以下将是该文件的内容。

module learnpackage

go 1.13  

module learnpackage 这一行指定了模块的名称为learnpackage 。正如我们前面提到的,learnpackage 将是导入该模块内创建的任何包的基本路径。go 1.13 这一行指定本模块中的文件使用go版本1.13

创建简单兴趣的自定义包

属于一个包的源文件应该放在各自独立的文件夹中。Go中的一个惯例是用与包相同的名称来命名这个文件夹。

让我们在learnpackage 文件夹内创建一个名为simpleinterest 的文件夹。mkdir simpleinterest 将为我们创建这个文件夹。

simpleinterest文件夹内的所有文件都应该以package simpleinterest 行开始,因为它们都属于simpleinterest 包。

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

以下将是我们应用程序的目录结构。

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

simpleinterest.go 文件中加入以下代码。

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
}

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

请注意,函数名称Calculate以大写字母开头。这一点至关重要,我们很快就会解释为什么需要这样做。

导入自定义包

要使用一个自定义包,我们必须首先导入它。导入路径是模块的名称加上包的子目录和包的名称。在我们的例子中,模块名称是learnpackage ,包simpleinterestsimpleinterest 文件夹中,直接在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)
}

上面的代码导入了simpleinterest 包,并使用Calculate 函数来查找简单利息。 标准库中的包不需要模块名称前缀,因此 "fmt "不需要模块前缀也能工作。当应用程序运行时,输出将是

Simple interest calculation  
Simple interest is 500  

关于go安装的更多信息

现在我们了解了包是如何工作的,现在是时候多谈一点关于go install 。像go install 这样的Go工具是在当前目录下工作的。让我们来理解这意味着什么。到现在为止,我们一直在从目录~/Documents/learnpackage/ 中运行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 ),如果包存在于运行它的当前目录或父目录或它的父目录中,它就会尝试编译主函数,等等。

我们在Documents 目录下,那里没有go.mod 文件,因此go install 抱怨说它找不到包learnpackage

当我们移动到~/Documents/learnpackage/ 时,那里有一个go.mod 文件,我们的模块名称是go.mod 文件中的learnpackage

因此,go install learnpackage 将在~/Documents/learnpackage/ 目录内工作。

但是到目前为止,我们只是使用了go install ,我们没有指定包的名称。如果没有指定软件包名称,go install 将默认为当前工作目录下的模块名称。这就是为什么当go install ,而不使用~/Documents/learnpackage/ 的任何包名时,它可以工作。因此,以下3条命令在运行时是等效的。~/Documents/learnpackage/

go install

go install .

go install learnpackage  

我还提到,go install 有递归搜索父目录中go.mod文件的能力。让我们检查一下这是否有效。

cd ~/Documents/learnpackage/simpleinterest/  

上述命令将把我们带到simpleinterest 目录。在该目录下运行

go install learnpackage  

Go安装将成功地在父目录learnpackage 中找到一个go.mod file ,该目录已经定义了模块learnpackage ,因此它可以工作:)。

输出的名称

我们在Simple interest包中把函数Calculate 大写。这在Go中有着特殊的含义。任何以大写字母开头的变量或函数在Go中都是导出的名字。只有导出的函数和变量可以从其他包中访问。在我们的例子中,我们想从主包中访问Calculate 函数。因此,它被大写了。

如果在simpleinterest.go 中把函数名从Calculate 改为calculate ,如果我们试图在main.go中用simpleinterest.calculate(p, r, t) 来调用这个函数,编译器会出错

# learnpackage
./main.go:13:8: cannot refer to unexported name simpleinterest.calculate
./main.go:13:8: undefined: simpleinterest.calculate

因此,如果你想访问一个包之外的函数,它应该被大写。

init函数

Go中的每个包都可以包含一个init 函数。 init 函数不能有任何返回类型,也不能有任何参数。init函数不能在我们的源代码中明确调用。它将在包被初始化时被自动调用。init函数的语法如下

func init() {  
}

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

一个包的初始化顺序如下

  1. 包级变量首先被初始化
  2. 接下来调用init函数。一个包可以有多个init函数(可以在一个文件中,也可以分布在多个文件中),它们按照呈现给编译器的顺序被调用。

如果一个包导入了其他的包,导入的包会先被初始化。

一个包将只被初始化一次,即使它是从多个包中导入的。

让我们对我们的应用程序做一些修改,以了解init 函数。

首先,让我们在simpleinterest.go 文件中添加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
}

我们已经添加了一个简单的init函数,它只是打印出Simple interest package initialised

现在让我们修改一下主包。我们知道,在计算单利时,本金、利率和时间长度应大于零。我们将在main.go 文件中使用init函数和包级变量定义这个检查。

main.go 修改为以下内容。

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

以下是对main.go 所做的修改

  1. prt变量从主函数级移到包级。
  2. 增加了一个init函数。如果本金、利率或时间长度小于零,init函数会打印日志并使用log.Fatal函数终止程序的执行。

初始化的顺序如下。

  1. 首先对导入的包进行初始化。因此,simpleinterest包首先被初始化,它的init方法被调用。
  2. 包级变量prt接下来被初始化。
  3. init函数在main中被调用。
  4. main函数最后被调用。

如果你运行该程序,你将得到以下输出。

Simple interest package initialized  
Main package initialized  
Simple interest calculation  
Simple interest is 500  

正如预期的那样,simpleinterest 包的init函数首先被调用,然后是包级变量的初始化p,rt 。接下来,主包的init函数被调用。它检查p,rt 是否小于零,如果条件为真则终止。我们将在另一个教程中详细学习if 语句。现在你可以假设,if p < 0 将检查p 是否小于0,如果是,程序将被终止。我们为rt 写了一个类似的条件。在这种情况下,所有这些条件都是假的,程序继续执行。最后,主函数被调用。

让我们稍微修改一下这个程序,以学习init函数的使用。

将行

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

中的 "main.go "改为

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

我们已经将p 初始化为负数。

现在如果你运行这个程序,你会看到

Simple interest package initialized  
Main package initialized  
2020/02/15 21:25:12 Principal is less than zero  

p是负数。因此,当init函数运行时,程序在打印完Principal is less 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 ,使错误消音。我们应该跟踪这些类型的错误消音器,并在应用程序开发结束时删除它们,包括导入的包,如果该包不被使用。因此,我们建议在导入语句之后,在包的层次上写上错误消音器。

有时我们需要导入一个包,以确保初始化的发生,即使我们不需要使用该包的任何函数或变量。init 例如,我们可能需要确保simpleinterest 包的函数被调用,尽管我们不打算在代码中使用该包。在这种情况下也可以使用_空白标识符,如下图所示。

package main

import (  
    _ "learnpackage/simpleinterest"
)

func main() {

}

运行上述程序将输出Simple interest package initialized 。我们已经成功地初始化了simpleinterest 包,尽管它在代码中没有任何地方被使用。

关于包的问题就到此为止。希望你喜欢阅读。请留下您的宝贵意见和反馈:)。