了解 Go 中软件包的可见性

介绍

在创建 Go 包时,最终的目标通常是让其他开发人员使用该包,无论是在高级包或整个程序中。通过 导入包,您的代码可以作为其他更复杂的工具的构建块。

在这种情况下,可见性意味着可以参考一个包或其他构件的文件空间,例如,如果我们在函数中定义一个变量,则该变量的可见性(范围)仅在其定义的函数内。

仔细控制包的可见性在编写 ergonomic 代码时很重要,尤其是在考虑未来您可能想要对您的包进行更改时。如果您需要修复错误、改进性能或更改功能,您将希望以一种方式进行更改,从而不会破坏任何使用您的包的代码。 减少破坏性更改的一种方法是只允许访问您包的必要部分。 通过限制访问,您可以对您的包进行内部更改,而不太可能影响其他开发人员如何使用您的包。

在本文中,您将学习如何控制包的可见性,以及如何保护您的代码的部分,应该只在您的包中使用. 为此,我们将创建一个基本的日志器来记录和调试消息,使用具有不同程度的项目可见性的包。

前提条件

要遵循本文中的示例,您将需要:

1.
2├── bin 
34└── src
5    └── github.com
6        └── gopherguides

出口和未出口物品

與其他程式語言如Java和Python(https://www.digitalocean.com/community/tutorial_series/how-to-code-in-python-3)不同,這些語言使用「公用」、「私有」或「保護」等 _access modifiers 來指定範圍,Go 會通過聲明的方式來決定一個項目是否是「輸出」和「未輸出」的。

所有声明,如类型,变量,常数,函数等,以大字母开头的声明都可见于当前包的外部。

让我们来看看下面的代码,仔细关注资本化:

 1[label greet.go]
 2package greet
 3
 4import "fmt"
 5
 6var Greeting string
 7
 8func Hello(name string) string {
 9    return fmt.Sprintf(Greeting, name)
10}

此代码声明它在问候包中,然后声明两个符号,一个叫做问候的变量和一个叫做问候的函数,因为它们都以大写开始,它们都导出,并可用于任何外部程序。

定义包的可见性

为了更仔细地了解软件包可见性在程序中是如何工作的,让我们创建一个日志包,考虑到我们想要在软件包之外看到的内容,以及我们不会看到的内容。这个日志包将负责将我们的任何程序消息记录到控制台。

首先,在您的src目录中,让我们创建一个名为logging的目录,将我们的日志文件放入:

1mkdir logging

然后移动到下一个目录:

1cd logging

然后,使用像nano这样的编辑器创建一个名为logging.go的文件:

1nano logging.go

在我们刚刚创建的logging.go文件中插入以下代码:

 1[label logging/logging.go]
 2package logging
 3
 4import (
 5    "fmt"
 6    "time"
 7)
 8
 9var debug bool
10
11func Debug(b bool) {
12    debug = b
13}
14
15func Log(statement string) {
16    if !debug {
17    	return
18    }
19
20    fmt.Printf("%s %s\n", time.Now().Format(time.RFC3339), statement)
21}

此代码的第一行声明了一个名为logging的包。在这个包中,有两个导出的函数:DebugLog。这些函数可以被导入logging包的任何其他包调用。还有一个名为debug的私人变量。这个变量只能从logging包中访问。重要的是要注意,虽然函数Debug和变量debug都具有相同的拼写,但函数是资本化的,而变量不是。

保存并删除文件。

要在我们的代码的其他区域中使用这个包,我们可以 import it into a new package 创建这个新包,但我们首先需要一个新的目录来存储这些源文件。

让我们离开日志目录,创建一个名为cmd的新目录,然后进入这个新目录:

1cd ..
2mkdir cmd
3cd cmd

在我们刚刚创建的cmd目录中创建一个名为main.go的文件:

1nano main.go

现在我们可以添加以下代码:

 1[label cmd/main.go]
 2package main
 3
 4import "github.com/gopherguides/logging"
 5
 6func main() {
 7    logging.Debug(true)
 8
 9    logging.Log("This is a debug statement...")
10}

但是,在我们可以运行这个程序之前,我们还需要创建几个配置文件,以便我们的代码正常工作。Go 使用 Go Modules来配置导入资源的包依赖性。Go 模块是在您的包目录中放置的配置文件,告诉编译器从哪里导入包裹。虽然学习有关模块超出本文的范围,我们可以写几个配置行,以使此示例在本地工作。

cmd目录中打开以下go.mod文件:

1nano go.mod

然后将以下内容放入文件中:

1[label go.mod]
2module github.com/gopherguides/cmd
3
4replace github.com/gopherguides/logging => ../logging

该文件的第一行告诉编译器cmd包具有github.com/gopherguides/cmd的文件路径,第二行告诉编译器github.com/gopherguides/logging包可以在../logging目录中在磁盘上找到。

我们还需要一个go.mod文件用于我们的logging包,让我们回到logging目录并创建一个go.mod文件:

1cd ../logging
2nano go.mod

将以下内容添加到文件中:

1[label go.mod]
2module github.com/gopherguides/logging

这告诉编译器,我们创建的日志包实际上是github.com/gopherguides/logging包,这使得我们可以将该包导入我们的包,并使用我们之前所写的以下行:

 1[label cmd/main.go]
 2package main
 3
 4import "github.com/gopherguides/logging"
 5
 6func main() {
 7    logging.Debug(true)
 8
 9    logging.Log("This is a debug statement...")
10}

您现在应该有以下目录结构和文件布局:

1├── cmd
2│   ├── go.mod
3│   └── main.go
4└── logging
5    ├── go.mod
6    └── logging.go

现在我们已经完成了所有配置,我们可以使用以下命令从cmd包中运行程序:

1cd ../cmd
2go run main.go

您将获得类似于以下的输出:

1[secondary_label Output]
22019-08-28T11:36:09-05:00 This is a debug statement...

该程序将以 RFC 3339 格式打印当前时间,随后发送给日志器的任何声明。

由于DebugLog函数是从日志包中导出的,所以我们可以在我们的包中使用它们,但是,在日志包中导出的debug变量不会导出。

添加以下突出的行到main.go:

 1[label cmd/main.go]
 2package main
 3
 4import "github.com/gopherguides/logging"
 5
 6func main() {
 7    logging.Debug(true)
 8
 9    logging.Log("This is a debug statement...")
10
11    fmt.Println(logging.debug)
12}

保存并运行该文件. 您将收到类似于以下的错误:

1[secondary_label Output]
2. . .
3./main.go:10:14: cannot refer to unexported name logging.debug

现在我们已经看到包装中的导出未导出项目的行为,接下来我们将看看如何从结构导出领域方法

结构内可见性

虽然我们在上一节中构建的日志器中的可见性方案可能适用于简单的程序,但它共享过多的状态,以至于在多个包内有用。这是因为导出的变量可以访问多个包,可以将变量修改成矛盾的状态。允许你的包的状态以这种方式改变,很难预测你的程序将如何行为。使用当前的设计,例如,一个包可以将Debug变量设置为true,另一个包可以在同一情况下设置为false

我们可以通过创建一个结构来隔离记录仪,然后将方法挂掉,这将使我们能够创建一个记录仪的实例,以便在每个消耗它的包中独立使用。

日志包更改为以下,以重构代码并隔离日志:

 1[label logging/logging.go]
 2package logging
 3
 4import (
 5    "fmt"
 6    "time"
 7)
 8
 9type Logger struct {
10    timeFormat string
11    debug bool
12}
13
14func New(timeFormat string, debug bool) *Logger {
15    return &Logger{
16    	timeFormat: timeFormat,
17    	debug:      debug,
18    }
19}
20
21func (l *Logger) Log(s string) {
22    if !l.debug {
23    	return
24    }
25    fmt.Printf("%s %s\n", time.Now().Format(l.timeFormat), s)
26}

在这个代码中,我们创建了一个Logger结构,这个结构将容纳我们未出口的状态,包括要打印的时间格式和debug变量设置为truefalse函数设置了创建日志器的初始状态,例如时间格式和调试状态。然后它将我们内部给出的值存储在未出口的变量timeFormatdebug中。我们还创建了一种名为Log的方法,在Logger类型上采用了我们想要打印的陈述。

这种方法将允许我们在许多不同的包中创建一个日志器,并独立于其他包如何使用它。

要在另一个包中使用它,让我们更改 cmd/main.go 以看起来如下:

 1[label cmd/main.go]
 2package main
 3
 4import (
 5    "time"
 6
 7    "github.com/gopherguides/logging"
 8)
 9
10func main() {
11    logger := logging.New(time.RFC3339, true)
12
13    logger.Log("This is a debug statement...")
14}

运行此程序将为您提供以下输出:

1[secondary_label Output]
22019-08-28T11:56:49-05:00 This is a debug statement...

在此代码中,我们通过将导出函数称为来创建日志器的实例,我们将该实例的引用存储在日志器变量中,现在我们可以调用logging.Log来打印陈述。

如果我们尝试从Logger中引用一个未导出的字段,例如timeFormat字段,我们会收到编译时间错误,然后尝试添加以下突出的行并运行cmd/main.go:

 1[label cmd/main.go]
 2
 3package main
 4
 5import (
 6    "time"
 7
 8    "github.com/gopherguides/logging"
 9)
10
11func main() {
12    logger := logging.New(time.RFC3339, true)
13
14    logger.Log("This is a debug statement...")
15
16    fmt.Println(logger.timeFormat)
17}

这会导致以下错误:

1[secondary_label Output]
2. . .
3cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)

编译器承认logger.timeFormat不被导出,因此无法从logging包中获取。

方法内可见性

与结构字段一样,方法也可以导出或导出。

为了说明这一点,让我们将 leveled logging 添加到我们的日志器中。 Leveled logging 是对您的日志进行分类的一种方式,以便您可以搜索特定类型的事件。

*信息级别,它代表信息类型事件,通知用户的行动,如程序启动电子邮件发送。这些帮助我们调试和跟踪我们程序的部分,以查看是否有预期行为发生 *警告级别。这些类型的事件识别出什么时候发生了意想不到的事情,而不是错误,如电子邮件未发送,重复发送。它们帮助我们看到我们的程序的部分不像我们预期的那样顺利 *错误级别,这意味着程序遇到问题,如文件未找到

您可能还希望打开和关闭某些级别的日志记录,特别是如果您的程序不按预期运行,并且您想对程序进行调试。我们将通过更改程序添加此功能,以便当调试设置为时,它将打印所有级别的消息。

通过对logging/logging.go进行以下更改来添加平衡日志:

 1[label logging/logging.go]
 2
 3package logging
 4
 5import (
 6    "fmt"
 7    "strings"
 8    "time"
 9)
10
11type Logger struct {
12    timeFormat string
13    debug bool
14}
15
16func New(timeFormat string, debug bool) *Logger {
17    return &Logger{
18    	timeFormat: timeFormat,
19    	debug:      debug,
20    }
21}
22
23func (l *Logger) Log(level string, s string) {
24    level = strings.ToLower(level)
25    switch level {
26    case "info", "warning":
27    	if l.debug {
28    		l.write(level, s)
29    	}
30    default:
31    	l.write(level, s)
32    }
33}
34
35func (l *Logger) write(level string, s string) {
36    fmt.Printf("[%s] %s %s\n", level, time.Now().Format(l.timeFormat), s)
37}

在本示例中,我们引入了日志方法的新论点,现在我们可以通过日志消息的级别日志方法决定它是哪个级别的消息。如果它是信息警告消息,而调试字段是真实,那么它会写出消息,否则它会忽略消息。

大部分确定消息是否被打印的逻辑存在于日志方法中,我们还引入了一种名为的非导出方法,而方法实际上是输出日志消息的方法。

我们现在可以通过更改cmd/main.go来在我们的其他包中使用这种平衡日志,以显示如下:

 1[label cmd/main.go]
 2package main
 3
 4import (
 5    "time"
 6
 7    "github.com/gopherguides/logging"
 8)
 9
10func main() {
11    logger := logging.New(time.RFC3339, true)
12
13    logger.Log("info", "starting up service")
14    logger.Log("warning", "no tasks found")
15    logger.Log("error", "exiting: no work performed")
16
17}

运行这将给你:

1[secondary_label Output]
2[info] 2019-09-23T20:53:38Z starting up service
3[warning] 2019-09-23T20:53:38Z no tasks found
4[error] 2019-09-23T20:53:38Z exiting: no work performed

在本示例中,cmd/main.go 成功地使用了导出的日志方法。

现在我们可以通过将debug转换为false来传输每个消息的级别:

 1[label main.go]
 2package main
 3
 4import (
 5    "time"
 6
 7    "github.com/gopherguides/logging"
 8)
 9
10func main() {
11    logger := logging.New(time.RFC3339, false)
12
13    logger.Log("info", "starting up service")
14    logger.Log("warning", "no tasks found")
15    logger.Log("error", "exiting: no work performed")
16
17}

现在我们将看到只有错误级别的消息打印:

1[secondary_label Output]
2[error] 2019-08-28T13:58:52-05:00 exiting: no work performed

如果我们尝试从日志包外调用方法,我们会收到编译时间错误:

 1[label main.go]
 2package main
 3
 4import (
 5    "time"
 6
 7    "github.com/gopherguides/logging"
 8)
 9
10func main() {
11    logger := logging.New(time.RFC3339, true)
12
13    logger.Log("info", "starting up service")
14    logger.Log("warning", "no tasks found")
15    logger.Log("error", "exiting: no work performed")
16
17    logger.write("error", "log this message...")
18}
1[secondary_label Output]
2cmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)

当编译器看到您试图引用从另一个以下列字母开头的软件包的内容时,它就会知道它没有被导出,因此会引入编译器错误。

本教程中的日志器说明了我们如何编写代码,只暴露了我们希望其他包的部分。因为我们控制了包的哪些部分在包外可见,我们现在可以进行未来的更改,而不会影响任何代码,这取决于我们的包。例如,如果我们只想关闭信息级别的消息,当调试是假的,你可以做这个更改,而不会影响你的API的任何其他部分。

结论

本文展示了如何在包之间共享代码,同时还可以保护您的包的实施细节,这允许您导出一个简单的API,它很少会改变后向兼容性,但将允许您在包中根据需要进行私有更改,以便在未来更好地工作。

要了解更多关于 Go 中的软件包的信息,请参阅我们的 Importing Packages in GoHow To Write Packages in Go 文章,或探索我们的整个 How To Code in Go 系列

Published At
Categories with 技术
Tagged with
comments powered by Disqus