你需要知道的关于 Go 包的一切

在 Go 语言中包管理和部署的完整概述

如果您熟悉 JavaNodeJS 等语言,那么您可能非常熟悉。一个包只不过是一个包含一些代码文件的目录,它从一个单一的引用点公开不同的变量( 特点 )。让我来解释一下这是什么意思。

假设您有上千个函数,在处理任何项目时都需要它们。其中一些函数具有共同的行为。例如,toUpperCase 和 toLowerCase 函数转换了 stringcase,因此你可以将它们写入单个文件中( 可能是 case.go )。还有其他函数可以对 string 数据类型执行其他操作,因此也可以将它们写入单独的文件中。

由于您有许多文件可以处理 string 数据类型,所以您创建了一个名为 string 的目录,并将所有与 string 相关的文件放入其中。最后,将所有这些目录放在一个父目录中,该目录将是您的包。整个包结构如下所示。

package-name
├── string
|  ├── case.go
|  ├── trim.go
|  └── misc.go
└── number
   ├── arithmetics.go
   └── primes.go

我将详细解释如何从包中导入函数和变量,以及如何将所有内容混合在一起形成一个包,但是现在,将您的包想象成一个包含 .go 文件的目录。

每一个 Go 程序都必须是某个包的一部分。正如 开始学习 Go 这个课程所讨论的,一个独立的可执行 Go 程序必须有 package main 声明。如果一个程序是main 包的一部分,那么 go install 将创建一个二进制文件;它在执行时调用程序的 main 函数。如果一个程序不是 main 包的一部分,那么使用 go install 命令创建一个 package archive 文件。不要担心,我将在后面的主题中解释这些

让我们创建一个可执行包。众所周知,要创建一个二进制可执行文件,我们需要我们的程序是 main 包的一部分,它必须有 main 函数,这是执行的入口点。

包名是 src 目录中包含的目录名。在上面的例子中,app 是包,因为 appsrc 目录的子目录。因此,go install app 命令在 GOPATHsrc 目录中查找 app 子目录。然后编译包并在 bin 目录中创建 app 的二进制可执行文件,因为 bin目录在 PATH 中,所以改文件应该可以在终端上执行。

包声明应该是像上面例子 package main 第一行代码一样,可以不同于包的名称。因此,您可能会发现一些包的名称(目录的名称)与包声明不同。当您导入一个包时,将使用包声明来创建包引用变量,本文后面将对此进行解释。

go install <package> 命令在给定的 package 目录中查找 main 包声明的任何文件。如果它找到了一个文件,那么Go 知道这是一个可执行程序,它需要创建一个二进制文件。一个包可以有许多文件,但只有一个文件具有 main 函数,因为该文件将是执行的入口点。

如果一个包不包含 main 包声明的文件,那么 Go 会在 pkg 目录中创建一个 package archive (.a) 文件。

因为,app 不是一个可执行的包,所以它在 pkg 目录中创建 app.a 文件。我们不能执行这个文件,因为它不是一个二进制文件。

包的命名约定

Go 社区建议对包使用简单明了的名称。例如,strutils 用于 string 通用 方法或 http 用于 HTTP 请求相关函数。应该避免使用 under_scoreshy-phensmixedCaps 这类的包名。


创建包

正如我们所讨论的,有两种类型的包。一种是 可执行包,另一种是 通用包。可执行包是您的主要应用程序,因为您将运行它。通用包不是自执行的,相反,它通过提供通用函数和其他重要内容来增强可执行程序包的功能。

众所周知,一个包只有一个目录,让我们在 src 中创建 greet 目录,并在其中创建几个文件。这一次,我们将在每个文件的顶部编写一个 package greet 声明,声明这是一个通用包。

导出成员

一个通用包应该为导入它的包提供一些变量。就像 JavaScript 中的 export 语法一样,如果一个变量名以大写字母开头,Go 就会导出这个变量。所有其他不以大写字母开头的变量都是包的私有变量。

从现在开始,我将在本文中使用变量来描述一个导出成员,但导出成员可以是任何类型的,如 constantmapfunctionstructarrayslice 等。

让我们从 day.go 文件中导出一个 greeting 变量

在上面的程序中,Morning 变量将从包中导出,但 morning 变量不会被导出,因为它以小写字母开头。

导入包

现在,我们需要一个可执行包,它将使用我们的 greet 包。让我们在 src 中创建一个 app 目录,并创建带有 main 包声明和 main 函数的 entry.go 文件。注意这里,Go 包没有像 Node 中 index.js输入文件命名系统。对于一个可执行包,一个带有 main 函数的文件是执行的入口文件。

要导入一个包,我们使用 import 语法,后面跟着包名

与其他编程语言不同,包名也可以是一个子路径,如一个目录/greet,Go会自动为我们解析 greet 包的路径,如前面的嵌套包的主题所示。

首先在 GOROOT/src 目录中搜索包目录,如果没有找到包,则查找 GOPATH/src。由于 fmt 包是位于 GOROOT/src 的 Go 标准库的一部分,所以它是从那里导入的。由于 Go 在 GOROOT 中找不到 greet 包,它将在 GOPATH/src 中查找,我们会在那里找到它。

上面的程序抛出编译错误,因为 morning 变量在 greet 包中是私有的。如您所见,我们使用 . ( )符号来访问从包导出的成员。当您导入一个包时,Go 使用包的包声明创建一个全局变量。在上面的例子中,greet 是 Go 创建的全局变量,因为我们在 greet 包中包含的程序中使用了 package greet 声明。

我们可以使用分组语法( 括号 )将 fmtgreet 包导入在一组。这一次,我们的程序可以很好地编译,因为 Morning 变量可以从包外部获得。

嵌套包

我们可以将一个包嵌入到另一个包中。因为对于 Go 来说,包只是一个目录,就像在已经存在的包中创建子目录一样。我们所要做的就是提供嵌套包的相对路径。

编译包

如前所述,go run 命令编译并执行一个程序。我们知道,go install 命令编译包并创建二进制可执行文件或包存档文件。这是为了避免每次编译一个( 导入这些包的程序 )的时候都要编译这些包。go install 预编译一个包 并且 Go 指的是 .a 文件。

一般来说,当你安装一个第三方包时,Go 编译包并创建包存档文件。如果您在本地编写了包,那么您的 IDE 可能会在您将文件保存在包中或修改包时创建包存档。如果你安装了 Go 插件,VSCode 会在你保存时编译这个包


包初始化

当我们运行 Go 程序时,Go 编译器对包、包中的文件和包中的变量声明遵循一定的执行顺序。

包作用域

作用域是代码块中的一个区域,其中定义的变量是可访问的。包作用域是包中的一个区域,其中声明的变量可以从包中访问( 跨越包中的所有文件 )。这个区域是包中文件的最上方的部分。


看看 go run 命令。这一次,我们没有执行一个文件,而是使用了一个 glob 模式,将所有文件都包含在 app 包中以供执行。Go 足够聪明,可以找出应用程序的入口点,即 entry.go,因为它有 main 函数。我们也可以使用如下命令( 文件名顺序不重要 )。

go run src/app/version.go src/app/entry.go

go installgo build 命令需要一个包名,其中包含包内的所有文件,所以我们不必像上面那样指定它们。

回到我们的主要问题,我们可以在包的任何地方使用在 version.go 文件中声明的变量 version,即使它没有被导出( version ),因为它是在包作用域中声明的。如果 version 变量是在函数中声明的,那么它就不在包作用域内,上面的程序就无法编译。

不允许在同一个包中重新声明具有相同名称的全局变量. 因此,一旦声明了 version 变量,就不能在包范围内重新声明它。但是你可以在其他地方重新声明。

变量初始化

当一个变量 a 依赖于另一个变量 b 时,应事先定义 b,否则程序无法编译。Go 在函数内部遵循这个规则。

但是当这些变量在包作用域定义时,它们在初始化周期中声明。

在上面的例子中,首先声明了 c,因为它的值已经声明了。 在后面的初始化周期中,声明了b,因为它依赖于c,而 c 的值已经声明了。在最后的初始化周期中,a 被声明并赋值给 b。Go 可以像下面这样处理复杂的初始化周期。

在上面的例子中,首先声明 c,然后声明 b,因为它的值取决于 c,最后声明 a,因为它的值取决于 b。您应该避免如下所示的任何初始化循环,如下所示,初始化进入递归循环。

包作用域的另一个例子是,在一个单独的文件中有一个函数 f,它从 main 文件引用变量 c

初始化(init)函数

main 函数一样,init 函数在包初始化时被调用。它不接受任何参数,也不返回任何值。 init 函数是由 Go 隐式声明的,因此您不能从任何地方引用它( 或像 init() 那样调用它)。在一个文件或包中可以有多个 init 函数。init 函数在文件中的执行顺序将根据它们的出现顺序。

你可以在包的任何地方使用 init 函数。这些 init 函数按词法文件名顺序调用(字母顺序)。

毕竟,init 函数被执行,main 函数被调用。因此,init 函数的主要工作是初始化全局变量,这些变量在全局上下文中无法初始化。例如,初始化一个数组。

由于 for 语法在包范围内无效,我们可以使用 init 函数中的 for 循环来初始化大小为 10 的数组 integers

包别名

当您导入一个包时,使用包的声明来创建一个变量。如果您导入多个具有相同名称的包,这将导致冲突。

// parent.go
package greet
var Message = "Hey there. I am parent."
// child.go
package greet
var Message = "Hey there. I am child."

因此,我们使用包别名。我们在 import 关键字和包名之间声明一个变量名,它将成为引用包的新变量。

在上面的例子中,greet/greet 包现在被 child 变量引用。如果您注意到,我们在 greet 包中添加了一个下划线。 下划线是 Go 中的一个特殊字符,它充当 null 容器。 因为我们导入了 greet 包,但是没有使用它,所以 Go 编译器会报错。为了避免这种情况,我们将包的引用存储到 _ 中,Go 编译器将直接忽略它。

使用下划线来别名化一个包,这看起来没什么用,但是当你想要初始化一个包却不使用它的时候,这是非常有用的。

// parent.go
package greet

import "fmt"

var Message = "Hey there. I am parent."

func init() {
    fmt.Println("greet/parent.go ==> init()")
}

// child.go
package greet

import "fmt"

var Message = "Hey there. I am child."

func init() {
    fmt.Println("greet/greet/child.go ==> init()")
}

要记住的主要事情是,每个导入的包仅初始化一次。因此,如果包中有任何导入语句,则导入的包在主包执行的生命周期中只初始化一次。

如果我们使用 . (点)作为别名,如 import .「greet/greet」然后所有 greet 包的导出成员将在本地文件块范围内可用,我们可以引用 Message 而不使用限定词 child。因此,fmt.Println(Message) 可以正常工作。这种类型的导入被叫做点导入,然而 Go 社区 不太喜欢它,因为它会导致一些问题。


程序执行顺序

到目前为止,我们已经了解了关于包的一切。现在,让我们结合我们的理解来看 Go 程序是如何初始化的。

go run *.go
├── Main package is executed
├── All imported packages are initialized
|  ├── All imported packages are initialized (recursive definition)
|  ├── All global variables are initialized 
|  └── init functions are called in lexical file name order
└── Main package is initialized
   ├── All global variables are initialized
   └── init functions are called in lexical file name order

这里有一个小例子来证明它。

// version/get-version.go
package version
import "fmt"
func init() {
 fmt.Println("version/get-version.go ==> init()")
}
func getVersion() string {
 fmt.Println("version/get-version.go ==> getVersion()")
 return "1.0.0"
}
/***************************/
// version/entry.go
package version
import "fmt"
func init() {
 fmt.Println("version/entry.go ==> init()")
}
var Version = getLocalVersion()
func getLocalVersion() string {
 fmt.Println("version/entry.go ==> getLocalVersion()")
 return getVersion()
}
/***************************/
// app/fetch-version.go
package main
import (
 "fmt"
 "version"
)
func init() {
 fmt.Println("app/fetch-version.go ==> init()")
}
func fetchVersion() string {
 fmt.Println("app/fetch-version.go ==> fetchVersion()")
 return version.Version
}
/***************************/
// app/entry.go
package main
import "fmt"
func init() {
 fmt.Println("app/entry.go ==> init()")
}
var myVersion = fetchVersion()
func main() {
 fmt.Println("app/fetch-version.go ==> fetchVersion()")
 fmt.Println("version ===> ", myVersion)
}


安装第三方软件包

安装第三方包只是将远程代码克隆到本地的 src/<package> 目录中。不幸的是,Go 不支持包版本或提供包管理器,但有一个提议正在等待 在这里。(注:已经实现了可以看这里

因为 Go 没有一个集中的官方包注册表,所以它要求您提供包的主机名和路径。

$ go get -u github.com/jinzhu/gorm

上述命令从 http://github.com/jinzhu/gorm URL 导入文件并将其保存在 src/github.com/jinzhu/gorm 目录中。正如在嵌套包中讨论的,您可以像下面这样导入 gorm 包。

package main
import "github.com/jinzhu/gorm"
// use ==> gorm.SomeExportedMember

所以,如果你制作了一个包,并希望人们使用它,只要在 GitHub 上发布它,就可以了。如果您的包是可执行的,人们可以将它用作命令行工具,也可以将它导入程序并将其用作实用程序模块。他们唯一需要做的就是使用下面的命令。

$ go get github.com/your-username/repo-name

译自 medium

本作品采用《CC 协议》,转载必须注明作者和本文链接
最初的时候也是最苦的时候,最苦的时候也是最酷的时候。
本帖由系统于 1年前 自动加精
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!