Go Modules In Real Life - Go语言中文社区

Go Modules In Real Life


Go版本自从1.11版本发布以来就是Go中的新热点。如果你花时间在Go工作上,你就知道它是一种包含一些强大功能的简单语言。此外,如果你在1.11之前的任何时候都在使用Go工作,你就会知道臭名昭着的GOPATH。在学习和使用Go时,这对我来说是最令人困惑的事情之一。它只需要一个简单的谷歌搜索该术语,GOPATH以显示它对Go社区的挑战程度。可以这么说,Go开发人员对模块非常欣喜,因为它们解决了人们在之前版本中遇到的许多挫折。

但Go模块并非没有自己的复杂性和挑战。他们也没有历史博客和论坛帖子来帮助新人。我在团队项目中遇到的一个方面是如何以一种与一个或多个主程序共享公共包的方式组织代码。我花了一段时间才找到一个有效且看起来不像反模式的解决方案,但它在我们的项目中使这变得更加简单。所以我的希望是,在分享我们提出的建议时,它可能会为您省去一些麻烦。

或者你可以指出我们是如何做错的,这样我们就可以改进我们的代码。

Package与Module

“Module是Go相关Package的集合,它们作为一个单元一起进行版本化。”

  • Go Module Docs

首先,我想讨论Module和Package之间的区别。Go Module文档将模块描述为“作为单个单元一起版本化的相关Go Package 的Module”。因此,我们看到包是基本构建块,并且在某些时候,一个或多个包可以成为模块。

关于何时制作某个模块以及何时将其作为一个包放置,存在很多问题。回到我们的定义,有两个主要原因可以解释为什么你想要将一些代码作为自己的模块。

  1. 我们希望自己维护代码并使其远离其他应用程序版本
  2. 我们希望在一个或多个项目中或与公众共享该代码
  3. 我们有一个包含一个或多个程序的mono-repo代码库

我们将在这篇文章中讨论第三个原因。在这种情况下,您需要一些常用代码,而不是为每个子应用程序复制和粘贴。为此,我们可以使用我们称之为多模块的仓库。Go维护者创建了这个以允许同一模块的许多版本存在于同一个repo中。我们将以不同的方式使用它,我将很快介绍。

安装程序

为了说明我开始研究这个问题的背景,让我稍微描述一下我们的项目。

我们的项目有几个以Go作为AWS Lambda函数编写的webhook处理程序。这些lambdas从AWS上的简单队列服务(SQS)中提取消息并处理该消息。如果处理成功,则会从队列中删除。我们有一个包含所有lambda的repo,每个lambda都是它自己的可执行模块。但是,每个lambda都有一个共享逻辑的子集,它们之间共享。逻辑如数据模型,数据库连接代码和外部服务调用。它们都存在于同一个Github存储库中,并采用下面显示的文件结构。

/webhooks
    /webhookA (executable)
    /webhookB (executable)
    /db
    /models
    /api

我不想在每个webhook中放置共享代码的副本,也不想为不断增长的webhook处理程序列表维护一个单独的repo。所以我开始研究如何在所有lambda之间共享这段代码。在阅读大量文档的过程中,我将一些适用于我们项目的内容拼凑在一起,看起来并不像是一种简单的方法。

回顾一下,这是我们要完成的要求

  • 在单个文件夹和repo中维护Go项目的几个单独部分。在我们的例子中,这些部分是webhooks,但它们可以是独立的微服务,或者像SDK这样的一组库。
  • 允许共享公共代码存在于其自己的文件夹中,并由一个或多个主程序导入

这个例子

由于lambdas在没有部署到AWS的情况下运行和显示的工作量更多,我将使用一个人为的例子。我们在两个服务器使用的通用记录器模块旁边构建两个简单的Web服务器。

正如我上面提到的,要做到这一点,我们将创建所谓的多模块存储库。

所以没有进一步的麻烦......我们走了。

基本文件夹

注意:请确保按照此处的说明正确安装和配置Go 1.11或更高版本,因为我不会详细介绍Go设置或配置。

在顶层,我们将创建项目的开始。如果您正在跟随,请选择您之外的某个目录GOPATH以开始编写代码。

在该文件夹内创建以下文件夹结构:

/my_project
    /web_server
    /web_server_two
    /logger

基础模块

在每个目录中,我们将通过运行以下命令将它们初始化为Go模块:

go mod init my_project/web_server
go mod init my_project/web_server_two
go mod init my_project/logger

这将运行Go的内置脚本来创建go.mod跟踪模块依赖关系的文件。

在此之后,我们的文件夹结构现在应如下所示:

/my_project
    /web_server
        go.mod
    /logger
        go.mod
    /web_server_two
        go.mod

其中每个go.mod文件包含以下内容,模块名称分别换出:

module my_project/web_server

go 1.12

请注意,此时我们有一个repo,单个项目目录和三个单独的模块,其中一个模块可以在另外两个模块之间共享。

记录器

我们将从共享记录器模块开始,该模块将日志格式和功能抽象到一个单独的模块中。

logger目录下,创建一个logger.go包含以下代码的文件。

package logger

import (
    "fmt"
    "time"
)

// LogLevel is an enum-like type that we can use to designate the log level
type LogLevel int
const (
    DEBUG = iota
    INFO
    WARNING
    ERROR
)

// Logger is a base struct that could eventually maintain connections to something like bugsnag or logging tools
type Logger struct {}

// log is a private function that manages the internal logic about what and how to log data depending on the log level
func (l *Logger) log(level LogLevel, messages ...interface{}) {
    now := time.Now()
    var logType string
    switch level {
    case DEBUG:
        logType = "[DEBUG]"
        break
    case WARNING:
        logType = "[WARNING]"
        break
    case ERROR:
        logType = "[ERROR]"
        break
    default:
        logType = "[INFO]"
        break

    }
    // format the output in a somewhat friendly way
    fmt.Println("-----------------------------------------")
    fmt.Printf("%s - %sn", logType, now)
    for _, message := range messages {
        fmt.Printf("%+vn", message)
    }
    fmt.Println("-----------------------------------------")
}

// LogDebug is a publicly exposed info log that passes the message along correctly
func (l *Logger) LogDebug(messages ...interface{}) {
    l.log(DEBUG, messages...)
}

// LogInfo is a publicly exposed info log that passes the message along correctly
func (l *Logger) LogInfo(messages ...interface{}) {
    l.log(INFO, messages...)
}

// LogWarning is a publicly exposed info log that passes the message along correctly
func (l *Logger) LogWarning(messages ...interface{}) {
    l.log(WARNING, messages...)
}

// LogError is a publicly exposed info log that passes the message along correctly
func (l *Logger) LogError(messages ...interface{}) {
    l.log(ERROR, messages...)
}

我知道,这个记录器并没有赢得任何奖项,但它为我们的例子做了工作。

作为代码注释布局,我们有一个非常基本的枚举来显示日志级别,一个空结构来表示记录器,以及一些公开可用的函数来抽象出格式化逻辑。

现在我们准备开始在我们的其他应用程序中使用此模块。

Web服务器

第一个Web服务器将成为一个基本的hello world http服务器。为了使用我们的记录器,我要添加一个中间件功能,可以在每个请求上打印出很多内容。

要开始将目录更改为Web服务器模块并创建一个名为的文件main.go。现在记住这是一个可执行程序,因此它需要一个主程序包和主要功能来运行。

package main

import (
    "net/http"
)

func main() {
    http.Handle("/hello", http.HandlerFunc(handle))
}

func handle(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("world"))
}

好的,现在是有趣的部分。如您所见,我们需要一种方法来导入我们的共享记录器库。我们可以通过两种方式做到这一点。

选项1:我们将此代码推送到github并将其标记为已发布的版本号。根据它是公共还是私人仓库,我们还必须处理该go get命令的身份验证。这是一个非常有效的选择,说实话,90%的时间都是正确的方法。这里的缺点是,它真的是一个repo =模块,如上所述。由于我们的应用程序包含所有的webhooks,每个都是他们自己的模块,这意味着将它们分成单独的repos。我试图在一个仓库中保留多个已发布的模块,并准备抛弃整个代码库,因为它是如何无法维护的。

选项2:我将在这里展示的选项是创建一个多模块仓库并使用魔术词替换。这将告诉Go相对于go.mod文件的位置以在本地查找模块。

在此之前,我们忽略了go.mod我们在开始时生成的文件。这些操作告诉Go您正在该目录中构建模块。由于我们没有向任何模块添加任何外部依赖项go.mod,因此除了样板文件外,该文件仍为空。如果我们通过运行命令来说明Mongo数据库适配器

go get go.mongodb.org/mongo-driver/mongo

我们会看到Go更新我们的go.mod文件,看起来就像你在下面看到的那样

module my_project/web_server

go 1.12

require (
    github.com/go-stack/stack v1.8.0 // indirect
    github.com/golang/snappy v0.0.1 // indirect
    github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect
    github.com/xdg/stringprep v1.0.0 // indirect
    go.mongodb.org/mongo-driver v1.0.3 // indirect
    golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
    golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
    golang.org/x/text v0.3.2 // indirect
)

正如您所看到的,Go从Mongo的repo中删除了最新的标记版本,并将其依赖项添加到我们的文件系统中。然后,它go.mod为每个依赖项的文件添加了一条记录。该go.mod文件充当包清单,并跟踪每个模块的依赖关系。

我们拼图中缺少的一个功能让我花了一段时间才找到。我们可以在我们的go.mod文件中添加一个语句,告诉Go在哪里查找导入的模块。这样它就会看到而不是联系Github或其他一些包注册表。

我们使用文件中的replace关键字来执行此操作,该关键字go.mod告诉Go使用本地模块。让我们看看这个在行动。

目前,我们的共享记录器代码位于目录下my_project/logger。同样重要的是要注意调用此模块my_project/logger。您可以在go.mod文件的第一行中看到这一点,该行设置模块名称。

如果您尝试将此直接导入到您的Web服务器中,就像这样

package main

import (
    "net/http"
    "my_project/logger" // import logger
)

func main() {
    l := new(logger.Logger) // create and use a new logger
    l.LogError("Not Found")
    http.Handle("/hello", http.HandlerFunc(handle))
    http.ListenAndServe(":5500", nil)
}

func handle(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("world"))
}

你会收到错误的

build my_project/web_server: cannot load my_project/logger: cannot find module providing package my_project/logger

这是因为Go正在寻找一个url来下拉该记录器代码,除非你实际上已经在某个地方发布了它,否则它不存在。

但是,如果您在go.mod文件中只添加一行

replace my_project/logger => ../logger

Go神奇地知道相对于该目录中go.mod调用的模块
my_project/logger的文件../logger

现在再次运行我们的代码会记录我们的愚蠢Not Found错误并启动服务器。

-----------------------------------------
[ERROR] - 2019-07-02 08:41:49.511755 -0700 PDT m=+0.000826403
Not Found
-----------------------------------------

重新启动服务器时,将反映您对记录器模块所做的任何更改。您可以将该replace指令添加到此目录中的任何其他模块中。您可以在任何目录中执行此操作,但如果您从全部链接到本地​​模块,则表明您需要将其设置为独立模块。如果您的模块位于不同的存储库中,这将无效。

回到我们的示例,我们将实现一个中间件函数,该函数使用记录器打印出每个请求的url。然后我们将证明我们可以在不同的包中共享相同的代码。最后,我们将快速回顾一下您应该和不应该使用该replace指令的时间。

Loggerware

我们对Web服务器的最终编辑将是添加我们的loggerware函数,它应该类似于下面的代码,

package main

import (
    "net/http"
    "my_project/logger"
)

func main() {
    l := new(logger.Logger)
    // wrap our hello handler function
    http.Handle("/hello", loggerware(l, http.HandlerFunc(handle)))
    http.ListenAndServe(":5500", nil)
}

func handle(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("world"))
}

// loggerware can wrap any handler function and will print out the datetime of the request
// as well as the path that the request was made to.
func loggerware(l *logger.Logger, next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
        requestPath :=  r.URL
        l.LogInfo("Request Made To: ", requestPath)
    })
}

正如您所看到的,除了将我们的记录器集成到请求中之外,我们没有做任何超级复杂的事情。在更真实的场景中,我们希望将路由分成几组,并能够使用类似gorilla mux包的东西将记录器应用于所有端点。或者在类似GraphQL服务器的情况下,您可以使用它来包装GraphQL端点并为每个GraphQL请求创建日志。

这应该是为Web服务器做的。如果您运行此代码并打开浏览器,localhost:5500/hello您应该看到响应world回来以及看起来像这样漂亮的终端登录,

-----------------------------------------
[INFO] - 2019-07-02 08:55:34.360145 -0700 PDT m=+17.505867800
Request Made To:
/hello
-----------------------------------------

其他服务器

为了证明这是共享代码并且可以应用于任何其他服务器,我将快速构建另一个使用相同记录器的服务器,但是与我们的另一个完全隔离的服务。

现在移动到您的web_server_two文件夹将此代码放在一个名为的文件中main.go。记住要有一个可执行程序,你需要一个main带有函数的包main

这是此Web服务器的代码。

package main

import (
    "net/http"
    "my_project/logger"
)

func main() {
    l := new(logger.Logger)

    http.Handle("/valar-morghulis", loggerware(l, http.HandlerFunc(handle)))
    http.ListenAndServe(":5600", nil)
}

func handle(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("valar dohaeris"))
}

func loggerware(l *logger.Logger, next http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
        requestPath :=  r.URL
        l.LogInfo("Request Made To Server Two: ", requestPath)
    })
}

同样,不是最有用的服务器,但它证明我们的记录器代码可以编写一次,并通过我们的各种微服务作为共享但本地可用的库重用。

结论

把事情包裹起来,让我们记住我们的目标是什么。

  1. 维护一个包含所有微服务或功能的存储库作为独立的可执行文件
  2. 作为共享模块,在所有这些应用程序中共享Go代码的常见部分
  3. 避免使用各自的版本控制来维护许多不同的已发布模块

正如我们所看到的,我们能够使用称为mutli-module存储库的东西来完成这项工作。使用go.mod文件中的replace指令,我们指定了一个相对于共享模块的本地模块。然后使用我们的本地模块,而不是联系到Github或其他一些注册表。

遵循此模式,我们创建了一个记录器库作为封装模块。然后,我们可以在两个独立的,孤立的Web服务器上使用它。我们可以将此应用于任何存在包含一个或多个重用相同代码的模块的主存储库的场景。

什么时候不用

“除了高级用户以外的所有用户,您可能希望采用通常的惯例,即一个repo =一个模块。对于代码存储选项的长期演变而言,repo可以包含多个模块非常重要,但几乎肯定不是你想要默认执行的操作。“

  • Russ Cox(Google开发人员和核心Go贡献者)

您不应该使用此方法来替代版本控制和分发模块。

正如Go模块文档中的核心Go开发人员Russ Cox所说,最常见的用例是一个模块=一个repo。如果您有可能在其他项目中使用您的模块,那么您应该考虑将其作为独立模块进行分发。事实上,本地导入仅在代码位于同一存储库中时才有效。

此解决方案仅在您维护单声道存储器时无效,并且无意将共享逻辑拉入单独的存储库。

好处是因为你将共享代码设置为一个真正的模块,将它分解为一个独立的模块是微不足道的。所需要做的就是从使用共享代码的任何模块中删除replace指令。然后从Github或其他一些注册表导入包。因为它非常简单,所以这种方法可以证明您可能对代码体系结构进行的任何大的更改。


非常感谢您阅读这篇文章!如果您喜欢这篇文章,请务必与他人分享。如果您有任何想法或问题,请发表评论。如果您想要在发布新文章时获得更新,您可以在我的网站上注册我的邮件列表。

转:https://medium.com/@ckeyes88/go-modules-in-real-life-87a21fb4d8aa

版权声明:本文来源简书,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://www.jianshu.com/p/1105b6cf39fb
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-01-08 22:08:33
  • 阅读 ( 1754 )
  • 分类:Go

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢