介绍
模块是Go集成到系统中,为依赖管理提供支持的解决方案。这意味着模块可以接触到与源代码有关的所有内容,包括对编辑器的支持。为了向编辑器提供对模块的支持(以及其他原因),Go团队构建了一个名为gopls的服务,该服务实现了语言服务器协议(LSP)
。LSP最初是由Microsoft为VS Code开发的,现已成为一个开放标准。该协议的核心思想是为编辑人员提供对编程语言的支持,例如代码自动补全,跳转到定义位置以及查找所有引用。
在你使用模块和VS Code时,在编辑器中单击“保存”将不再直接运行go build
命令。现在发生的事情是将请求发送到gopls,然后gopls运行适当的Go命令和关联的API,来为编辑器提供反馈和支持。Gopls也可以将信息发送到编辑器而无需请求。有时,由于LSP的性质或运行Go命令的固有延迟,编辑器中代码的更改会出现滞后或不同步的现象。Go团队正在努力推出gopls v1.0版本来处理这些极端情况,以便您可以获得最流畅的编辑体验。
在本文中,我将逐步介绍在项目中添加和删除依赖项的基本工作流程。这篇文章使用的是VS Code编辑器,gopls的0.2.0版和Go的1.13.3版。
模块缓存
为了保证项目快速构建以及保持项目中的依赖项为最新更新,Go维护了一份本地缓存,保存了所有已经下载的模块。我们可以在$GOPATH/pkg
找到这份缓存。如果你没有设置过GOPATH,那么会使用默认的GOPATH,目录是$HOME/go
。
注意:建议提供一个环境变量,以允许用户自定义模块缓存的位置。如果未更改,$ GOPATH / pkg将是默认值。
代码1
$HOME/code/go/pkg
$ ls -l
total 0
drwxr-xr-x 11 bill staff 352 Oct 16 15:53 mod
drwxr-xr-x 3 bill staff 96 Oct 3 16:49 sumdb
代码1显示了当前我本地的$GOPATH/pkg
文件夹内容。可以看到有两个文件夹,mod
和sumdb
。如果仔细查看mod
文件夹内部,则可以了解有关模块缓存布局的更多信息。
代码2
$HOME/code/go/pkg
$ ls -l mod/
total 0
drwxr-xr-x 5 bill staff 160 Oct 7 10:37 cache
drwxr-xr-x 3 bill staff 96 Oct 3 16:55 contrib.go.opencensus.io
drwxr-xr-x 40 bill staff 1280 Oct 16 15:53 github.com
dr-x------ 26 bill staff 832 Oct 3 16:50 go.opencensus.io@v0.22.1
drwxr-xr-x 3 bill staff 96 Oct 3 16:56 golang.org
drwxr-xr-x 4 bill staff 128 Oct 7 10:37 google.golang.org
drwxr-xr-x 7 bill staff 224 Oct 16 15:53 gopkg.in
drwxr-xr-x 7 bill staff 224 Oct 16 15:53 k8s.io
drwxr-xr-x 5 bill staff 160 Oct 16 15:53 sigs.k8s.io
代码2显示了当前模块缓存的顶层结构。我们可以看到与模块名称关联的URL的第一部分被用作模块缓存中的顶级文件夹。下面我进入github.com/ardanlabs
,我可以向你展示两个实际的模块。
代码3
$HOME/code/go/pkg
$ ls -l mod/github.com/ardanlabs/
total 0
dr-x------ 13 bill staff 416 Oct 3 16:49 conf@v1.1.0
dr-x------ 18 bill staff 576 Oct 12 10:08 service@v0.0.0-20191008203700-49ed4b4f1088
代码3显示了我在ArdanLabs中使用的两个模块及其版本。第一个是conf
模块,另一个模块与我用来教学kubernetes服务的项目相关。
gopls服务在内存中也维护了一份模块缓存。启动VS Code并进入模块模式后,gopls服务会启动来支持编辑器的会话。gopls内部会将模块缓存与磁盘上当前的内容同步。gopls正是使用此内部模块缓存来处理编辑器请求。
对于这篇文章,我将在开始之前清除模块缓存,以便拥有一个干净的工作空间。在启动VS Code编辑器之前,我还将设置项目。这将向你展示如何处理尚未下载到本地的模块缓存或在gopls内部的模块缓存中进行更新的情况。
注意:清除模块缓存是在任何常规工作流程中都不需要执行的操作。
代码4
$ go clean -modcache
代码4展示了如何清除磁盘上的本地模块缓存。go clean
传统上,用于清理本地GOPATH工作目录和GOPATH bin
文件夹。现在携带新的-modcache
标志,该命令可用于清除模块缓存。
注意:此命令不会清除任何正在运行的gopls实例的内部缓存。
新建项目
我将在GOPATH之外启动一个新项目,在编写代码的过程中,我将逐步介绍添加和删除依赖项的基本工作流程。
代码5
$ cd $HOME
$ mkdir service
$ cd service
$ mkdir cmd
$ mkdir cmd/sales-api
$ touch cmd/sales-api/main.go
代码5展示了设置工作目录的命令,创建初始项目结构并添加main.go
文件。
使用模块的第一步是初始化项目源代码树的根。这是一步通过使用go mod init
命令来完成。
代码6
$ go mod init github.com/ardanlabs/service
代码6展示了go mod init
的调用,将模块名称作为参数传递。如第一篇文章所述,模块名称允许内部导入并在模块内部解析。常见的命名方式是用托管代码的仓库的URL来命名模块。对于这篇文章,我假设此模块将与Github的Ardan Labs下的服务项目相关联。
调用go mod init
完成后,会在当前工作目录中创建一个go.mod
文件。该文件代表了项目的根目录。
代码7
01 module github.com/ardanlabs/service
02
03 go 1.13
代码7展示了此项目的初始模块文件的内容。完成后,该项目可以进行编码了。
代码8
$ code .
代码8展示了启动VS Code实例的命令。同时,这也将启动gopls服务的实例以支持该编辑器。
图1
图1展示了运行所有命令后VS Code编辑器中项目的效果。为了确保你使用的设置与我相同,这里列出了当前VS Code的设置。
代码9
{
// Important Settings
"go.lintTool": "golint",
"go.goroot": "/usr/local/go",
"go.gopath": "/Users/bill/code/go",
"go.useLanguageServer": true,
"[go]": {
"editor.snippetSuggestions": "none",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"gopls": {
"usePlaceholders": true, // add parameter placeholders when completing a function
"completeUnimported": true, // autocomplete unimported packages
"deepCompletion": true, // enable deep completion
},
"go.languageServerFlags": [
"-rpc.trace", // for more detailed debug logging
],
}
代码9显示了我当前的VS Code设置。如果你按照上面的步骤进行但没有看到相同的效果,请对照配置进行检查。如果你想查看当前推荐的VS Code设置,在这里。
开始编码
我将从该应用程序的初始代码开始。
代码10
https://play.golang.org/p/c8kGx7I9HJH
01 package main
02
03 func main() {
04 if err := run(); err != nil {
05 log.Println("error :", err)
06 os.Exit(1)
07 }
08 }
09
10 func run() error {
11 return nil
12 }
代码10展示了我添加在main.go
里的前12行代码。它设置了应用程序具有单一退出点的功能,并记录了启动或关闭时的任何错误。一旦将这12行代码保存到文件中,编辑器就会自动(感谢gopls)导入标准库中所需的依赖。
代码11
https://play.golang.org/p/x3hBA6PuW3R
03 import (
04 "log"
05 "os"
06 )
代码11展示了在03至06行对源代码所做的更改,这要归功于gopls与编辑器的集成。
接下来,我将添加对配置的支持。
代码12
https://play.golang.org/p/4hFXLJj4yT_Z
17 func run() error {
18 var cfg struct {
19 Web struct {
20 APIHost string `conf:"default:0.0.0.0:3000"`
21 DebugHost string `conf:"default:0.0.0.0:4000"`
22 ReadTimeout time.Duration `conf:"default:5s"`
23 WriteTimeout time.Duration `conf:"default:5s"`
24 ShutdownTimeout time.Duration `conf:"default:5s"`
25 }
26 }
27
28 if err := conf.Parse(os.Args[1:], "SALES", &cfg); err != nil {
29 return fmt.Errorf("parsing config : %w", err)
30 }
代码12展示了run
在第18至30行上添加到该函数中用来支持配置的代码。当此代码添加到源文件中并单击“保存”时,编辑器正确地将fmt
和time
包导入进来。不幸的是,由于gopls当前在其内部模块缓存中没有有关 conf
软件包的任何信息,因此gopls无法指导编辑器添加导入conf
或为编辑器提供软件包信息。
图2
image图2显示了编辑器清楚地表明它无法解析与conf
软件包有关的任何信息。
添加依赖
为了解决导入,需要检索包含conf
软件包的模块。一种方法是,将导入添加到源代码文件的顶部,然后让编辑器和gopls完成工作。
代码13
01 package main
02
03 import (
04 "fmt"
05 "log"
06 "os"
07 "time"
08
09 "github.com/ardanlabs/conf"
10 )
在代码13中,我在第09行中添加了conf
包的导入。一旦单击保存,编辑器通知gopls,然后gopls使用Go命令和关联的API查找,下载并提取该包的模块。这些调用还会更新Go模块文件以反映此更改。
代码14
~/code/go/pkg/mod/github.com/ardanlabs
$ ls -l
total 0
drwxr-xr-x 3 bill staff 96B Nov 8 16:02 .
drwxr-xr-x 3 bill staff 96B Nov 8 16:02 ..
dr-x------ 13 bill staff 416B Nov 8 16:02 conf@v1.2.0
代码14显示了Go命令如何完成其工作并下载了conf
1.2.0版的模块。现在,我们解决导入所需的代码在我的本地模块缓存中。
图3
image图3显示了编辑器如何仍然无法解析有关程序包的信息。为什么编辑器无法解析此信息?很不辛,gopls内部模块缓存与本地模块缓存不同步。gopls服务不知道Go命令刚刚进行的更改。由于gopls使用其内部缓存,因此gopls无法为编辑器提供所需的信息。
注意:此缺陷当前正在解决中,并将在即将发布的版本中修复。您可以在此处跟踪问题。(https://github.com/golang/go/issues/31999)
gopls内部模块缓存与本地模块缓存同步的一种快速方法是重新加载VS Code编辑器。这将重新启动gopls服务并重置其内部模块缓存。在VS Code中,有一个特殊的命令reload window
可以做到这一点。
Ctrl + Shift + P and run > Reload Window
图4[图片上传失败...(image-6a6146-1586268866514)]
图4显示了使用Ctrl + Shift + P
和键入reload window
后VS Code中出现的对话框。
运行此快捷命令后,导入关联的所有消息都会得到解决。
传递依赖
从Go工具的角度来看,构建此应用程序所需的所有代码现在都在本地模块缓存中。但是,conf
程序包依赖于Google 的go-cmp
程序包进行测试。
代码15
module github.com/ardanlabs/conf
go 1.13
require github.com/google/go-cmp v0.3.1
代码15显示了conf
模块1.2.0版的模块配置文件。我们可以看到conf
依赖 go-cmp
的0.3.1版本。该模块未在服务的模块文件中列出,因为这是多余的工作。Go工具可以遵循模块文件的路径,以获取构建或测试代码所需的所有模块的完整镜像。
此刻,该传递模块尚未找到、下载并提取到我的本地模块缓存中。由于构建代码时不需要此模块,因此Go生成工具尚未发现下载这个模块的意义。如果我在命令行上运行go mod tidy
,那么Go工具将会花费时间将go-cmp
模块加入本地缓存。
代码16
$ go mod tidy
go: downloading github.com/google/go-cmp v0.3.1
go: extracting github.com/google/go-cmp v0.3.1
代码16中显示了go-cmp
模块是如何找到、下载并提取出来。go mod tidy
命令不会更改项目的模块文件,因为这不是直接依赖项。它将更新go.sum
文件,以便记录模块的哈希值,以此来保证可以持久且可复制的构建。我将在以后的文章中讨论一下“校验和数据库”。
代码17
github.com/ardanlabs/conf v1.2.0 h1:2IntiqlEhRk+sYUbc8QAAZdZlpBWIzNoqILQvV6Jofo=
github.com/ardanlabs/conf v1.2.0/go.mod h1:ILsMo9dMqYzCxDjDXTiwMI0IgxOJd0MOiucbQY2wlJw=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
代码17显示了运行go mod tidy
后的校验和文件。与该项目关联的每个模块都有两个记录。
下载模块
如果您尚未准备好在代码库中使用特定模块,但想将模块提前下载到本地模块缓存中,一种方法是手动将模块添加到项目go.mod
文件中,然后在编辑器外部运行go mod tidy
。
代码18
01 module github.com/ardanlabs/service
02
03 go 1.13
04
05 require (
06 github.com/ardanlabs/conf v1.2.0
07 github.com/pkg/errors latest
08 )
在代码18中,可以看到我如何在模块文件中第7行手动添加的命令,用来获取最新版本的errors
模块。手动添加所需模块的重要部分是使用latest
标签。当go mod tidy
运行时发现此更改,它将告诉Go查找该errors
模块的最新版本并将其下载到我的缓存中。
代码19
$HOME/service
$ go mod tidy
go: finding github.com/pkg/errors v0.8.1
代码19显示了errors
0.8.1版本是如何找到、下载并提取。命令运行完成后,由于项目未使用该模块,因此将该模块会从模块文件中删除。但是,该模块已经记录在校验和文件中。
代码20
github.com/ardanlabs/conf v1.2.0 h1:2IntiqlEhRk+sYUbc8QAAZdZlpBWIzNoqILQvV6Jofo=
github.com/ardanlabs/conf v1.2.0/go.mod h1:ILsMo9dMqYzCxDjDXTiwMI0IgxOJd0MOiucbQY2wlJw=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
代码20显示了errors
现在如何在校验和文件中记录的。有一点很重要,校验和文件不是项目正在使用的所有依赖项的规范记录。它可以包含更多模块。
我更喜欢这种通过使用上面的方法来下载新模块而不是go get
,因为如果你不小心,go get
也可以升级项目的依赖关系图中的依赖关系(直接和间接)。重要的是要知道何时进行版本升级,而不仅仅是下载所需的新模块。在以后的文章中,我将讨论如何使用go get
更新现有模块的依赖。
删除依赖项
如果我决定不再使用conf
包,该怎么办?我可以删除使用了此包的任何代码。
代码21
https://play.golang.org/p/x3hBA6PuW3R
01 package main
02
03 import (
04 "log"
05 "os"
06 )
07
08 func main() {
09 if err := run(); err != nil {
10 log.Println("error :", err)
11 os.Exit(1)
12 }
13 }
14
15 func run() error {
16 return nil
17 }
代码21显示了conf
从main
函数中删除的代码。当我点击保存后,编辑器将从导入集中删除conf
的导入。但是,模块文件尚未更新。
代码22
01 module github.com/ardanlabs/service
02
03 go 1.13
04
05 require github.com/ardanlabs/conf v1.1.0
06
代码22显示代码仍然认为conf
软件包是必需的。要解决此问题,我需要离开编辑器并再次运行go mod tidy
。
代码23
$HOME/service
$ go mod tidy
代码23 显示了再次运行go mod tidy
的效果。这次没有输出。该命令完成后,模块文件将变得正确。
代码24
$HOME/services/go.mod
01 module github.com/ardanlabs/service
02
03 go 1.13
04
代码24显示该conf
模块已从模块文件中删除。这次go mod tidy
命令清除了校验和文件,它将为空。在你提交任何代码到VCS之前应该确保执行了go mod tidy
,并确保模块文件准确且与所使用的依赖项保持一致,这一点很重要。
结论
在不久的将来,不再需要我分享的这些变通办法,例如重新加载窗口。Go团队意识到了目前存在的这一缺陷和其他问题,因此他们正在积极努力解决所有缺陷。他们非常感谢Go问题跟踪上的所有反馈,因此,如果你发现了一些其他问题,请及时上报。问题无大小之分。作为社区,让我们与Go团队合作,快速解决这些剩余的问题。
目前正在研究的一项核心功能是gopls监视文件系统并查看项目更改的能力。这将有助于gopls使其内部模块缓存与磁盘上的本地模块缓存保持同步。一旦这个功能实现,就不需要再重新加载窗口了。后台进行工作时提供视觉提示的功能也在开发了。
总体而言,我对当前的工具集和重新加载窗口解决方法感到满意。希望你可以考虑使用模块。这些模块已准备就绪,可以使用,并且开始使用它的项目也越来越多了,Go生态系统对每个人来说会越来越好。
网友评论