I understand this is an old question, but there are some more details that are worth mentioning when managing multiple modules in one repository, with or without go.work
.
TL;DR
Each approach has pros and cons, but if you are working on a large code base with many modules, I'd suggest sticking to use version handling based on commits or tags, and use Go Workspace for your day to day development.
Go Module Details
replace
Directive with No Versioning
When you use replace
directive pointing to a local directory, you will find the version of the dependency module as v0.0.0-00010101000000-000000000000
. Essentially you get no version information.
With the main go.mod
defined with github.com/name/project
module path, github.com/name/project
module cannot make a reproducible build, because the dependency target for replace
directive may have had its content updated. This can be especially problematic if the dependency target of github.com/name/project/players
is used by many modules. Any change in such a common package can result in a behaviour change for all the dependents, all at the same time.
If that's not your concern, replace
directive should work absolutely fine. In such a setup, go.work
may be a layer you don't really need.
With Versioning
If you want to ensure version setup works for reproducible and deterministic build for multiple modules, you can take a few different approaches.
One go.mod
, one repository
This is probably the easiest approach. For each module, there is a clear commit history and versioning. As long as you refer to the module via remote repository, this is probably the easiest setup to start with, and dependency setup is very clear.
However, note that this approach would mean you'd need to manage multiple repositories, and making go.work
to help is going to require appropriate local directory mapping, which can be difficult for someone new to the code base.
Commit based versioning
It is still possible to deterministically define dependency with version information so that you can build your code, within a single repository. Commit based approach requires least step, and still works nicely. There are some catches to be noted, though.
- For
github.com/name/project
to have a dependency for github.com/name/project/players
, you need to ensure the code you need is in the remote repository. This is because github.com/name/project
will pull the code and commit information from the remote repository, even if the same code is available on your local copy of the repository. This ensures that the version of github.com/name/project/players
is taken from the commit reference, such as v0.1.1-0.20220418015705-5f504416395d
(ref: details of "pseudo-version")
- The module name must match up the directory structure. For example, if you have the single repository
github.com/name/project
, and module under /src/mymodule/
, the module name must be github.com/name/project/src/mymodule
. This is because when module path resolution takes place, Go finds the root of repository (in the above example, this would be github.com/name/project.git
), and then tries to follow the directory path based on the module name.
- If you are working in a private repository, you will need to ensure
go.sum
check doesn't block you. You can simply use GOPRIVATE=github.com/name/project
to specify paths you don't want the checksum verification to be skipped.
Tag based versioning
Instead of using the commit SHA, you can use Git tags.
But because there could be many modules in one repository, Go Module needs to find which tag maps to which. For example, with the following directory structure:
# All assumed to be using `github.com/name/project` prefix before package name
mypackage/ # v1.0.0
anotherpackage/ # v0.5.1
nested/dependency/ # v0.8.3
You will need to create tags in github.com/name/project
, named exactly to match the directory structure, such that:
mypackage/v1.0.0
anotherpackage/v0.5.1
nested/dependency/v0.8.3
This way, each tag is correctly referenced by Go Module, and your dependency can be kept deterministic.
go.work
Behaviour
If you have go.work
on a parent directory with go work use github.com/name/project/players
, etc., that takes precedence and uses the local files. This is even when you have a version specified in your go.mod
.
For local development, which spans across multiple projects, Go Workspace is a great way to work on multiple things at once, without needing to push the code change for the dependency only first. But at the same time, actual release will still require broken up commits, so that first commit can be referenced later in other code change.
go.work
is said to be a file you rarely need to commit to the repository. You must be aware of what the impact of having go.work
in parent paths would be, though.
--
References:
Side Note:
I have given a talk about this at Go Conference, hosted in Japan - you can find some demo code, slides, etc. here if you are curious to know more with examples.