Golang 1.18 workspaces 尝鲜

1,384 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情

前段时间发布的 Go 1.18 增加了 workspace 模式的支持,允许开发者同时在多个 module 进行开发。今天我们来了解一下 workspace 到底是什么,如何使用。

如果本地还没有ready,你可以在这里下载 1.18 版本,以及了解 Release Note

Workspaces

workspaces 支持开发者同时处理多个 module,而无需为每个 module 编辑 go.mod 文件。解决依赖关系时,工作空间中的每个 module 都被视为 root module。

此前,当我们需要将功能添加到一个 module 并在另一个 module 中使用它,我们有两个选项:

  1. 将更改发布到第一个 module;
  2. 使用replace编辑存在依赖 module 的 go.mod 文件以获取本地未发布的更改。当然,为了随后发布没有错误,我们还需要在发布本地更改后删除replace

有了 workspaces,我们可以使用 workspace 根目录中的 go.work 文件来控制所有依赖项。 go.work 文件具有覆盖各个 go.mod 文件的 usereplace 指令,因此无需单独编辑每个 go.mod 文件。

运行 go work init 即可创建一个 workspace,用空格分隔的一组module目录作为参数。workspace 不需要包含正在使用的 module。 init 命令会创建一个列出 workspace 中所有 module 的 go.work 文件。如果运行不带参数的 go work init,该命令会创建一个空工作区。

要将 module 添加到 workspace,请运行 go work use [module directory] 或手动编辑 go.work 文件。运行 go work use -r 将以递归方式将参数中所有带有 go.mod 的目录添加到 workspace 中。如果一个目录没有 go.mod 文件,或者不存在,那么 use 指令将把该目录的从 go.work 文件中删除。

go.work 语法类似于 go.mod 文件,包含以下指令:

  • go:go 版本,例如 1.18
  • use:将磁盘上的 module 添加到 workspace 的 main module 中。它的参数是包含 go.mod 目录的相对路径。
  • replace:与 go.mod 文件中的 replace 指令类似,go.work 文件中的 replace 指令将 module 的特定版本,或所有版本进行替换。

实战操作

光说不练假把式,其实作为一个长期使用 go mod 的开发者,直接看 Go 1.18 的介绍并不是非常直观。下面我们来尝试一下。开始前请确认本地 Go 版本已经升级到 >= 1.18。

了解一下 go work 能做什么

$ go work

Work provides access to operations on workspaces.

Note that support for workspaces is built into many other commands, not
just 'go work'.

See 'go help modules' for information about Go's module system of which
workspaces are a part.

See https://go.dev/ref/mod#workspaces for an in-depth reference on
workspaces.

See https://go.dev/doc/tutorial/workspaces for an introductory
tutorial on workspaces.

A workspace is specified by a go.work file that specifies a set of
module directories with the "use" directive. These modules are used as
root modules by the go command for builds and related operations.  A
workspace that does not specify modules to be used cannot be used to do
builds from local modules.

go.work files are line-oriented. Each line holds a single directive,
made up of a keyword followed by arguments. For example:

	go 1.18

	use ../foo/bar
	use ./baz

	replace example.com/foo v1.2.3 => example.com/bar v1.4.5

The leading keyword can be factored out of adjacent lines to create a block,
like in Go imports.

	use (
	  ../foo/bar
	  ./baz
	)

The use directive specifies a module to be included in the workspace's
set of main modules. The argument to the use directive is the directory
containing the module's go.mod file.

The go directive specifies the version of Go the file was written at. It
is possible there may be future changes in the semantics of workspaces
that could be controlled by this version, but for now the version
specified has no effect.

The replace directive has the same syntax as the replace directive in a
go.mod file and takes precedence over replaces in go.mod files.  It is
primarily intended to override conflicting replaces in different workspace
modules.

To determine whether the go command is operating in workspace mode, use
the "go env GOWORK" command. This will specify the workspace file being
used.

Usage:

	go work <command> [arguments]

The commands are:

	edit        edit go.work from tools or scripts
	init        initialize workspace file
	sync        sync workspace build list to modules
	use         add modules to workspace file

Use "go help work <command>" for more information about a command.

创建一个 module

  • 通过命令行创建一个目录
$ mkdir workspace
$ cd workspace
  • 初始化 module 我们的示例将会创建一个新的 module hello,依赖 golang.org/x/example
$ mkdir hello
$ cd hello
$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello

使用 go get 添加 golang.org/x/example 依赖

$ go get golang.org/x/example

此时生成的 go.mod 内容为

module example.com/hello

go 1.18

require golang.org/x/example v0.0.0-20220412213650-2e68773dfca0 // indirect

在 hello 目录下,创建一个 hello.go

package main

import (
    "fmt"

    "golang.org/x/example/stringutil"
)

func main() {
    fmt.Println(stringutil.Reverse("Hello"))
}

现在执行 go run example.com/hello,得到结果:olleH,这是字符串反转的正确结果。

创建 workspace

在上一步我们创建的workspace 目录下,执行

$ go work init ./hello

go work init 命令会创建一个 go.work 文件,其中包含了 ./hello 目录,内容如下

go 1.18

use ./hello

第一行的 go 1.18 和 go.mod 中并无差异,用意都在于告诉 Go 当前的文件应该以什么版本来解析。

第二行的 use 告诉 Go 在 ./hello 目录下的 module 应该作为 main module。这样一来,在 workspace 下的任何子目录,./hello 目录下的 module 都可用。

我们可以尝试在 workspace 目录下直接运行 go run example.com/hello,结果同样是olleH

这样的机制,使得我们可以在一个 module 的目录之外引用它的 package

当然,如果你直接在 workspace 目录下运行 go run 是会报错的,因为 go 也不知道此时该用哪个 module。

下载并修改 golang.org/x/example module

workspace 目录下clone 仓库

$ git clone https://go.googlesource.com/example
Cloning into 'example'...
remote: Total 165 (delta 27), reused 165 (delta 27)
Receiving objects: 100% (165/165), 434.18 KiB | 1022.00 KiB/s, done.
Resolving deltas: 100% (27/27), done.

把 clone 下来的 module 加入我们的 workspace

$ go work use ./example

此时 go.work 的内容变成:

go 1.18

use (
    ./hello
    ./example
)

同时包含了 example.com/hello 以及 golang.org/x/example 两个 module。

这样我们就可以直接使用本地 golang.org/x/example 的实现,而不是通过 go get 从远程获取的。

下面我们尝试在本地的 golang.org/x/example/stringutil 目录下,增加一个将字符串转为大写的函数 ToUpper

workspace/example/stringutil 目录下创建一个新文件 toupper.go

package stringutil

import "unicode"

// ToUpper uppercases all the runes in its argument string.
func ToUpper(s string) string {
    r := []rune(s)
    for i := range r {
        r[i] = unicode.ToUpper(r[i])
    }
    return string(r)
}

修改我们的 workspace/hello/hello.go,从原来的反转字符串,改成将 hello 字符串修改为大写:

package main

import (
    "fmt"

    "golang.org/x/example/stringutil"
)

func main() {
    fmt.Println(stringutil.ToUpper("Hello"))
}

现在,当我们重新回到workspace目录,依然运行 go run example.com/hello,你会发现,结果变成了 HELLO

$ go run example.com/hello
HELLO

这是因为,go.work 引入了 ./hello 以及./example 两个目录。

当执行 go run example.com/hello 时,从 ./hello 找到 example.com/hello module。

同样的,从 ./example 找到了 golang.org/x/example 依赖。

所以,在涉及多个 module 并行开发时,go.work 可以替代replace的作用。只要这些 module 在同一个 workspace 下,就可以很容易地在一个 module 中进行修改,在另一个 module 中直接使用。

补充几个官方提到的常用的命令:

  • go work use [-r] [dir] adds a use directive to the go.work file for dir, if it exists, and removes the use directory if the argument directory doesn’t exist. The -r flag examines subdirectories of dir recursively.
  • go work edit edits the go.work file similarly to go mod edit
  • go work sync pushes the dependencies in the go.work file back into the go.mod files of each workspace module.

在GOPATH中使用 workspace

如果你依然在使用 $GOPATH,可以参照下面的步骤来创建一个 workspace:

  • 在 GOPATH 的根目录执行 go work init
  • 如果需要使用一个 local module,执行 go work use [path-to-module]
  • 如果需要将 GOPATH 下所有 module 都添加到 workspace,可以执行 go work use -r,这样会递归地将所有带有 go.mod 的目录都添加到 workspace 中。

参考资料

Get familiar with workspaces

Tutorial: Getting started with multi-module workspaces