介绍
在 Go 中创建 包时,最终目标通常是使包可供其他开发人员使用,无论是高阶包还是整个程序。 通过导入包,您的代码可以作为其他更复杂工具的构建块。 但是,只有某些包可用于导入。 这是由包的可见性决定的。
Visibility 在此上下文中表示可以引用包或其他构造的文件空间。 例如,如果我们在函数中定义一个变量,该变量的可见性(范围)仅在定义它的函数内。 同样,如果您在包中定义变量,则可以使其仅对该包可见,或者也允许它在包外可见。
在编写符合人体工程学的代码时,仔细控制包的可见性很重要,尤其是在考虑到您可能希望对包进行的未来更改时。 如果您需要修复错误、提高性能或更改功能,您需要以不会破坏任何使用您的包的人的代码的方式进行更改。 最小化重大更改的一种方法是只允许访问包中正确使用它所需的部分。 通过限制访问,您可以在内部对您的包进行更改,而不会影响其他开发人员如何使用您的包。
在本文中,您将学习如何控制包的可见性,以及如何保护只应在包内使用的部分代码。 为此,我们将创建一个基本的记录器来记录和调试消息,使用具有不同程度项目可见性的包。
先决条件
要遵循本文中的示例,您将需要:
- 按照 如何安装 Go 并设置本地编程环境 设置的 Go 工作区。 本教程将使用以下文件结构:
. ├── bin │ └── src └── github.com └── gopherguides
出口和未出口的项目
与 Java 和 Python 等其他程序语言不同,它们使用 访问修饰符 例如 public
、private
或 protected
来指定范围, Go 通过它的声明方式来确定一个项目是否是 exported
和 unexported
。 在这种情况下,导出一个项目会使其 visible
在当前包之外。 如果它没有被导出,它只能在它定义的包中可见和可用。
这种外部可见性是通过将声明的项目的第一个字母大写来控制的。 所有以大写字母开头的声明,例如 Types
、Variables
、Constants
、Functions
等,在当前包之外都是可见的。
让我们看下面的代码,注意大小写:
问候.go
package greet import "fmt" var Greeting string func Hello(name string) string { return fmt.Sprintf(Greeting, name) }
此代码声明它在 greet
包中。 然后它声明了两个符号,一个名为 Greeting
的变量和一个名为 Hello
的函数。 因为它们都以大写字母开头,所以它们都是 exported
并且可用于任何外部程序。 如前所述,制作一个限制访问的包将允许更好的 API 设计,并使内部更新包变得更容易,而不会破坏依赖于包的任何人的代码。
定义包可见性
为了更深入地了解包可见性在程序中的工作原理,让我们创建一个 logging
包,记住我们希望在包外显示的内容和不显示的内容。 这个日志包将负责将我们的任何程序消息记录到控制台。 它还将查看我们正在记录的 级别 。 级别描述了日志的类型,将是以下三种状态之一:info
、warning
或 error
。
首先,在您的 src
目录中,让我们创建一个名为 logging
的目录来放置我们的日志文件:
mkdir logging
接下来进入该目录:
cd logging
然后,使用 nano 之类的编辑器,创建一个名为 logging.go
的文件:
nano logging.go
将以下代码放入我们刚刚创建的 logging.go
文件中:
日志记录/logging.go
package logging import ( "fmt" "time" ) var debug bool func Debug(b bool) { debug = b } func Log(statement string) { if !debug { return } fmt.Printf("%s %s\n", time.Now().Format(time.RFC3339), statement) }
该代码的第一行声明了一个名为 logging
的包。 在这个包中,有两个exported
函数:Debug
和Log
。 这些函数可以被任何其他导入 logging
包的包调用。 还有一个名为 debug
的私有变量。 此变量只能从 logging
包中访问。 需要注意的是,虽然函数 Debug
和变量 debug
的拼写相同,但函数是大写的,而变量不是。 这使得它们具有不同范围的不同声明。
保存并退出文件。
要在我们代码的其他区域使用这个包,我们可以将它导入一个新的包。 我们将创建这个新包,但首先需要一个新目录来存储这些源文件。
让我们移出 logging
目录,创建一个名为 cmd
的新目录,然后进入该新目录:
cd .. mkdir cmd cd cmd
在我们刚刚创建的 cmd
目录中创建一个名为 main.go
的文件:
nano main.go
现在我们可以添加以下代码:
cmd/main.go
package main import "github.com/gopherguides/logging" func main() { logging.Debug(true) logging.Log("This is a debug statement...") }
我们现在已经编写了整个程序。 然而,在我们可以运行这个程序之前,我们还需要创建几个配置文件以使我们的代码正常工作。 Go 使用 Go Modules 来配置导入资源的包依赖。 Go 模块是放置在你的包目录中的配置文件,它告诉编译器从哪里导入包。 虽然学习模块超出了本文的范围,但我们只需编写几行配置即可使此示例在本地工作。
打开cmd
目录下的如下go.mod
文件:
nano go.mod
然后将以下内容放入文件中:
去.mod
module github.com/gopherguides/cmd replace github.com/gopherguides/logging => ../logging
这个文件的第一行告诉编译器cmd
包的文件路径是github.com/gopherguides/cmd
。 第二行告诉编译器包 github.com/gopherguides/logging
可以在本地磁盘的 ../logging
目录中找到。
我们的 logging
包还需要一个 go.mod
文件。 让我们回到 logging
目录并创建一个 go.mod
文件:
cd ../logging nano go.mod
将以下内容添加到文件中:
去.mod
module github.com/gopherguides/logging
这告诉编译器我们创建的 logging
包实际上是 github.com/gopherguides/logging
包。 这使得可以使用我们之前编写的以下行将包导入我们的 main
包中:
cmd/main.go
package main import "github.com/gopherguides/logging" func main() { logging.Debug(true) logging.Log("This is a debug statement...") }
您现在应该具有以下目录结构和文件布局:
├── cmd │ ├── go.mod │ └── main.go └── logging ├── go.mod └── logging.go
现在我们已经完成了所有配置,我们可以使用以下命令从 cmd
包中运行 main
程序:
cd ../cmd go run main.go
您将获得类似于以下内容的输出:
Output2019-08-28T11:36:09-05:00 This is a debug statement...
该程序将以 RFC 3339 格式打印出当前时间,然后是我们发送给记录器的任何语句。 RFC 3339 是一种时间格式,旨在表示 Internet 上的时间,通常用于日志文件。
因为 Debug
和 Log
函数是从 logging 包中导出的,我们可以在我们的 main
包中使用它们。 但是,logging
包中的 debug
变量不会被导出。 尝试引用未导出的声明将导致编译时错误。
将以下突出显示的行添加到 main.go
:
cmd/main.go
package main import "github.com/gopherguides/logging" func main() { logging.Debug(true) logging.Log("This is a debug statement...") fmt.Println(logging.debug) }
保存并运行该文件。 您将收到类似于以下内容的错误:
Output. . . ./main.go:10:14: cannot refer to unexported name logging.debug
现在我们已经了解了包中的 exported
和 unexported
项的行为,接下来我们将了解如何从 structs
。
结构内的可见性
虽然我们在上一节中构建的记录器中的可见性方案可能适用于简单的程序,但它共享的状态太多,无法在多个包中使用。 这是因为导出的变量可供多个包访问,这些包可能会将变量修改为相互矛盾的状态。 允许以这种方式更改包的状态使得很难预测程序的行为方式。 例如,在当前设计中,一个包可以将 Debug
变量设置为 true
,而另一个包可以在同一实例中将其设置为 false
。 这会产生问题,因为导入 logging
包的两个包都会受到影响。
我们可以通过创建一个结构然后挂起方法来隔离记录器。 这将允许我们创建一个记录器的 instance
以在每个使用它的包中独立使用。
将 logging
包更改为以下内容以重构代码并隔离记录器:
日志记录/logging.go
package logging import ( "fmt" "time" ) type Logger struct { timeFormat string debug bool } func New(timeFormat string, debug bool) *Logger { return &Logger{ timeFormat: timeFormat, debug: debug, } } func (l *Logger) Log(s string) { if !l.debug { return } fmt.Printf("%s %s\n", time.Now().Format(l.timeFormat), s) }
在这段代码中,我们创建了一个 Logger
结构。 该结构将容纳我们未导出的状态,包括要打印的时间格式和 true
或 false
的 debug
变量设置。 New
函数设置创建记录器的初始状态,例如时间格式和调试状态。 然后它将我们在内部提供的值存储到未导出的变量 timeFormat
和 debug
。 我们还在 Logger
类型上创建了一个名为 Log
的方法,该方法接受我们要打印的语句。 在 Log
方法中是对其局部方法变量 l
的引用,以访问其内部字段,例如 l.timeFormat
和 l.debug
。
这种方法将允许我们在许多不同的包中创建一个 Logger
并独立于其他包如何使用它来使用它。
要在另一个包中使用它,让我们将 cmd/main.go
更改为如下所示:
cmd/main.go
package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, true) logger.Log("This is a debug statement...") }
运行此程序将为您提供以下输出:
Output2019-08-28T11:56:49-05:00 This is a debug statement...
在这段代码中,我们通过调用导出的函数 New
创建了一个记录器的实例。 我们将对该实例的引用存储在 logger
变量中。 我们现在可以调用 logging.Log
来打印语句。
如果我们尝试从 Logger
引用未导出的字段,例如 timeFormat
字段,我们将收到编译时错误。 尝试添加以下突出显示的行并运行 cmd/main.go
:
cmd/main.go
package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, true) logger.Log("This is a debug statement...") fmt.Println(logger.timeFormat) }
这将给出以下错误:
Output. . . cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)
编译器识别出 logger.timeFormat
未导出,因此无法从 logging
包中检索。
方法内的可见性
与结构字段一样,方法也可以导出或不导出。
为了说明这一点,让我们将 leveled 日志记录添加到我们的记录器中。 分级日志记录是一种对日志进行分类的方法,以便您可以在日志中搜索特定类型的事件。 我们将放入记录器的级别是:
info
级别,表示通知用户某个操作的信息类型事件,例如Program started
或Email sent
。 这些帮助我们调试和跟踪程序的某些部分,以查看是否正在发生预期的行为。warning
级别。 这些类型的事件可识别何时发生非错误的意外事件,例如Email failed to send, retrying
。 他们帮助我们看到我们的计划中没有像我们预期的那样顺利的部分。error
级别,表示程序遇到问题,如File not found
。 这通常会导致程序运行失败。
您可能还希望打开和关闭某些级别的日志记录,特别是如果您的程序没有按预期执行并且您想要调试程序。 我们将通过更改程序来添加此功能,以便当 debug
设置为 true
时,它将打印所有级别的消息。 否则,如果是false
,它只会打印错误信息。
通过对 logging/logging.go
进行以下更改来添加分级日志记录:
日志记录/logging.go
package logging import ( "fmt" "strings" "time" ) type Logger struct { timeFormat string debug bool } func New(timeFormat string, debug bool) *Logger { return &Logger{ timeFormat: timeFormat, debug: debug, } } func (l *Logger) Log(level string, s string) { level = strings.ToLower(level) switch level { case "info", "warning": if l.debug { l.write(level, s) } default: l.write(level, s) } } func (l *Logger) write(level string, s string) { fmt.Printf("[%s] %s %s\n", level, time.Now().Format(l.timeFormat), s) }
在此示例中,我们为 Log
方法引入了一个新参数。 我们现在可以传入日志消息的 level
。 Log
方法确定它是什么级别的消息。 如果是 info
或 warning
消息,并且 debug
字段为 true
,则写入消息。 否则它会忽略该消息。 如果它是任何其他级别,例如 error
,它将无论如何都会写出消息。
Log
方法中大部分判断消息是否打印的逻辑。 我们还介绍了一种未导出的方法,称为 write
。 write
方法是实际输出日志消息的方法。
我们现在可以通过将 cmd/main.go
更改为如下所示,在我们的其他包中使用此级别的日志记录:
cmd/main.go
package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, true) logger.Log("info", "starting up service") logger.Log("warning", "no tasks found") logger.Log("error", "exiting: no work performed") }
运行它会给你:
Output[info] 2019-09-23T20:53:38Z starting up service [warning] 2019-09-23T20:53:38Z no tasks found [error] 2019-09-23T20:53:38Z exiting: no work performed
在此示例中,cmd/main.go
成功使用了导出的 Log
方法。
我们现在可以通过将 debug
切换为 false
来传入每条消息的 level
:
main.go
package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, false) logger.Log("info", "starting up service") logger.Log("warning", "no tasks found") logger.Log("error", "exiting: no work performed") }
现在我们将看到只有 error
级别的消息打印:
Output[error] 2019-08-28T13:58:52-05:00 exiting: no work performed
如果我们尝试从 logging
包外部调用 write
方法,我们将收到编译时错误:
main.go
package main import ( "time" "github.com/gopherguides/logging" ) func main() { logger := logging.New(time.RFC3339, true) logger.Log("info", "starting up service") logger.Log("warning", "no tasks found") logger.Log("error", "exiting: no work performed") logger.write("error", "log this message...") }
Outputcmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)
当编译器看到您试图从另一个以小写字母开头的包中引用某些内容时,它知道它没有被导出,因此会引发编译器错误。
本教程中的记录器说明了我们如何编写只公开我们希望其他包使用的部分的代码。 因为我们控制了包的哪些部分在包外可见,所以我们现在能够在不影响任何依赖于我们包的代码的情况下进行未来的更改。 例如,如果我们只想在 debug
为 false 时关闭 info
级别的消息,您可以在不影响 API 的任何其他部分的情况下进行此更改。 我们还可以安全地更改日志消息以包含更多信息,例如运行程序的目录。
结论
本文展示了如何在包之间共享代码,同时保护包的实现细节。 这允许您导出一个简单的 API,该 API 很少更改以实现向后兼容性,但将允许根据需要在包中进行私有更改,以使其在未来更好地工作。 在创建包及其相应的 API 时,这被认为是最佳实践。
要了解有关 Go 中包的更多信息,请查看我们的 在 Go 中导入包和 如何在 Go 中编写包的文章,或探索我们的整个 如何在 Go 中编码系列[ X185X]。