https://go.dev/ref/mod#workspaces describes the current implementation of Go workspaces, as proposed by https://golang.org/issue/45713, which culminated in the proposal doc at https://go.googlesource.com/proposal/+/master/design/45713-workspace.md.

The proposal doc says:

go.work files should not be checked into version control repos containing modules so that the go.work file in a module does not end up overriding the configuration a user created themselves outside of the module. The go.work documentation should contain clear warnings about this.

However, the canonical documentation about go.work at https://go.dev/ref/mod#workspaces makes no mention of this, as far as I can see. In practice, it seems like the common knowledge among Go developers is that the file should never be checked in, because:

  • GitHub's default gitignore includes the file since https://github.com/github/gitignore/pull/3884
  • Official Go repositories set an example by not checking them in; for example, x/tools does not include one, even though it has two modules connected by a directory replace directive
  • Some third party docs or blog posts echo the recommendation to never check them in, like https://sebastian-holstein.de/post/2021-11-08-go-1.18-features/

First, we should decide whether we actually want to recommend users to never check in go.work files, like the original proposal doc said. If that's the case, then we simply need to add that recommendation to the /ref/mod page.

However, if we don't want to make that general recommendation, then we need to figure out under what circumstances it is recommended or not to check in go.work into VCS. For example, here are some personal guesses of mine:

  • For a VCS repository holding a Go library module intended for external use, checking in go.work is likely a bad idea, as it prevents downstream users from easily setting up their own go.work in a parent directory.
  • For large monorepos with multiple modules, it likely makes sense to check in go.work, as it makes the setup easier and often has no downsides.
  • In some cases, go.work files will naturally live in the parent directory to the VCS clones, so they have no VCS repository to naturally be checked into at all.

cc @bcmills @marwan-at-work @matloob

Comment From: mvdan

@bcmills points out that perhaps there isn't consensus about whether to check go.work files into VCS. We never gave any advice on it, and the community seems to lean on "never check them in".

We could recommend never checking them in, but I think that's a bad idea: in some particular scenarios it can make perfect sense to check them in, such as monorepos holding multiple modules.

We could continue with no recommendation at all, but I worry that then we'll continue to see third party docs and sites make assertive recommendations like "never check them in".

I personally think that we should give some general guidance that isn't too strict one way or another, such as:

go.work files should generally not be checked into VCS, as their purpose is to develop multiple modules at the same time, and each module tends to have its own VCS repository. However, there are certain scenarios where it can make sense to check in go.work files, such as a monorepo holding multiple modules which depend on each other.

This needs a decision that isn't easy to make, and we should probably get input from others, so @bcmills suggests turning this issue into a proposal. Doing that.

Comment From: omertuc

I've noticed that gopls isn't able to "Find references" for an identifier in a "nested" module unless I create a go.work file. Without the go.work file it only finds the declaration/definition itself. With the go.work file it successfully finds all references in all modules in the repo. You can try to "Find references" in this example project and see it works only while the go.work file exists.

I couldn't get VSCode or Neovim to make it work in any other way - I just had to create a go.work file - even with multiple VSCode "LSP workspaces". Maybe I did something wrong, feel free to correct me.

I would hate to have to teach people in my team how to create a go.work file just so gopls will work as expected for them, so it seems very reasonable to just push it into VCS. That's why I was very surprised to see the proposal suggested against doing that.

@mvdan I'm not sure if my example project is what you were referring to as "monorepos", but wanted to give it as an example

Maybe it's just a gopls bug and so this can't be used as an argument in favor of checking in go.work files to VCS, but I'm not familiar enough with gopls to say for sure

Comment From: mvdan

Assuming your nested Go modules all live in the same git repository as the main module, then yes, that's the sort of setup that I mean by a monorepo. A VCS monorepo will typically contain dozens of modules, and in general nested modules might be tricky and could be avoided, but the end result is still the same: it is possible and perhaps reasonable to check in a go.work file.

Comment From: bep

The go.work feature is incredibly useful, but since it's default on (and cannot be turned off without deleting the work file (I think)), I think you should expand your documentation scope to also discuss where to put the go.work file.

If you put it inside your Go module (monorepo or not) and add go.work to your .gitignore, your local go test ./... may be very different from the CI build.

My current working theory about my own use of go.work files is that they cannot live in the Go repos themselves, but typically in separate /myworkspace folders.

Comment From: bcmills

and cannot be turned off without deleting the work file (I think)

FWIW, you should be able to set GOWORK=off to force it off. (See https://go.dev/ref/mod#environment-variables.)

Comment From: bep

FWIW, you should be able to set GOWORK=off to force it off.

Those environment variables does not automatically propagate across all the environments in an open source project.

I have said this before, but may as well repeat it, I think having this enabled by default was the wrong decision. We had a situation where you had to make sure that any replacements in go.mod had to be short lived and certainly not committed to Git. Now it's very much guess work what Go Module state you build with. Again, I salute the concept of go.work, but it should be something that was off by default and turned on by switches (e.g. in VS Code or by GOWORK=on).

Comment From: Southclaws

I've come across quite a few situations where I've written a library and made use of a go.work file to provide a tangible benefit: I can easily have sub-modules, run go test ./..., have gopls work and not pollute the repo's package go.mod with unnecessary dependencies.

Two use-cases I had recently: I wanted to write some tests for a package that works with errors. These tests want to ensure my library works nicely with all the other errors packages. So this means I'm importing 10 different errors packages into that ./tests/go.mod. I used a go.work file in the root to make everything play nicely and the root level go.mod has no dependencies, so that's really neat.

The other case was I wrote a package that integrates with various other packages. Of course each binding layer is a separate module so, again, the root level go.mod doesn't need to unnecessarily specify a ton of unrelated dependencies when, realistically, a consumer of that package is only ever going to want either zero or one of the integrations packages. Now, I could create a ton of repos, or make a github org, but I simply don't want to incur the administrative burden so a monorepo suits me well. Again, go.work makes this all work nicely with gopls, go test, godoc, etc.

I thought this was fine until someone on the Gopher slack told me this was bad practice, I couldn't really find any definitive answer and was linked to here. All of what I did was based on official documentation, not blog posts or anything else. So it wasn't really clearly communicated that what I was doing (despite making my workflow easier) was actually not the intended use of go.work.

So it seems I've stumbled on some use cases that make my workflow easier, but are considered bad practice. This feels very un-Golang. I'd rather the tooling allow me to do things that are good practice, not not allow them at all, so there's no room for opinions and discussion (like the K&R thing - no arguments, just one style.)

Thanks for raising this though! I'm keen to see a definitive outcome, and I'm not strongly opinionated one way or the other. 😊

Comment From: jeffwidman

@Southclaws for both of your scenarios, when a user imports your package and then creates their own go.mod file, go mod tidy will prune that project's transitive dependencies such that the unused test modules and unused integrations packages will never be imported... you can check, and you shouldn't see anything actually listed as // indirect unless it's actually used.

So I don't think either scenario you describe really makes sense for a go.work module, you're basically putting in manual effort to do what the tooling already does automatically.

Comment From: Southclaws

Ah thanks, I didn't know this! I figured the lack of explicit dependencies/devDependencies (like npm) meant this wasn't the case.

Given that, I see no reason to not ignore go.work files, it would be worth mentioning this in the official docs for sure!

Comment From: matteoolivi

I can testify that in the aforementioned monorepo use case (in the first comment in this issue for example) committing go.work is helpful.

I have a git mono-repo with two library modules (they might become more, and with dependencies between them) and one executable module that uses the two libraries. Whatever the checked out revision of the repo, the executable should depend on the version of the two libraries at the same revision (as opposed to some fixed tag/version). We want this to hold true for every developer in the team and for the automation that operates on the repo (e.g. CI). This property makes versioning, integration and dependency management much easier. For such a use case, committing go.work achieves the desired result and ensures consistency across all developers and automation.

Comment From: DeedleFake

I'm confused. From the way you described it, it sounds like you've split three packages in a repo into their own submodules and then used a go.work to imperfectly recreate what having all three packages in the same module would have done anyways.

Comment From: sparr

I work on https://github.com/awslabs/soci-snapshotter/tree/main which includes https://github.com/awslabs/soci-snapshotter/tree/main/cmd

It seems that each person working on this repository needs to create a go.work so that gopls (and, thus, vscode) and other tools will behave as expected. Committing the obvious simple go.work to the repo would reduce the repetition of that effort. I am still struggling to understand why we should not do that.

Comment From: SealOfTime

I have just found myself in need of writing a very small code-generating utility, that has dependencies on several libraries, that are otherwise undesirable to have in the main project's go.mod. As such, I've came up with two solutions. First one is to have an external repo with literally one file and a go.mod, have a separate lifecycle for that utility and the generated code, and as such constantly struggle with keeping track of whether that dependency is in the correct version in the main repo. And the latter one is to have a nested go.mod, so that utility may have a clear set of dependencies, different from the main project. Which causes a lot of issues if go.work is not present in repo's root. Thus, I believe, we either are in need of support for nested go.mods or approval of including go.works in VCS

Comment From: DeedleFake

I have just found myself in need of writing a very small code-generating utility, that has dependencies on several libraries, that are otherwise undesirable to have in the main project's go.mod.

Why is it undesirable?

so that utility may have a clear set of dependencies

Is this so that it can be utilized by some other tool? The go.mod file doesn't actually define a list of dependencies of a package. It simply defines rules by which versions of dependencies of packages in that module should be selected. The actual set of dependencies of a package is determined by what it actually imports in the code, not by the contents of the associated go.mod file.

Comment From: dylan-bourque

We've just run into a rough edge case related to this.

We have some automation that uses a go list command similar to below to find other direct internal module dependencies that have available updates.

go list -e -u -m -f '{{if (and (not (or .Main .Indirect)) .Update)}}{{.Path}}@{{.Update.Version}}{{end}}' github.com/example/...

We've been using this successfully for a long time for single-module repos.

What we've seen, though, is that if there is a go.work file in the workspace then all dependencies are showing as indirect and our go list command returns no results.

Is this expected/desired behavior for go list when go.work is present?

Comment From: MarcMeszaros

I have the same issue that @dylan-bourque mentioned with multiple modules in a single repo.

Comment From: thepudds

I just re-read this briefly, but I didn't see a link to one of the older comments from Russ on this topic from the original proposal discussion: https://github.com/golang/go/issues/45713#issuecomment-891325796.

Note there is some nuance there (including talking about not yet having enough experience at that point in time, possibly giving some advice without being overly proscriptive, and so on).

Sorry if that was already linked here.

Comment From: mvdan

For what it's worth I think it would be fine to fix this issue by updating the docs to say that checking in the file is okay in some cases. Right now we are silent, and there's widespread "common knowledge" that dictates the file should never be checked in. We should at least clarify that that's not the official recommendation.

Comment From: matloob

I think we should strongly caution against checking in go.work files, but there are some rare cases where it makes sense to check them in so we have to leave some room for that possibility. Here's proposed wording to add to ref/mod. What do y'all think:

go.work files should usually not be checked into version control though there may be rare cases
where it might make sense to do so: particularly if the workspace modules don't have any other
modules that depend on them. Module developers should think very carefully before checking in
`go.work` files whether it is worth doing so because of the considerable disadvantages:
Checked-in `go.work` files might override developers' own `go.work` files contained
in parent directories and cause confusion when the developers' `go.work` files don't apply. Checked-in
`go.work` files may also result in the wrong versions of modules being tested in continuous integration
systems: `go.work` will override the versions of dependency modules that are in workspaces so those
versions will not be respected in testing.

Comment From: eliben

This would be great, thanks @matloob

I'd maybe organize the text a bit differently (*), but the overall idea is solid.

(*) Start by saying it's not recommended and list the disadvantages as a bullet list. Then after the list, add a disclaimer for when the guideline can be relaxed.

Comment From: mvdan

Compared to my ealier attempt at https://github.com/golang/go/issues/53502#issuecomment-1204134618, I like that it explains a couple of pitfalls with checked-in go.work files. I agree with @eliben that I would like to see better formatting, perhaps splitting into 1-2 paragraphs with a bullet list.

However, I'm not a big fan of the wording "rare cases"; I think monorepos (or multi-module repos) are relatively common, particularly in work scenarios. I worry that only listing disadvantages, and saying that there "may be rare cases where it might make sense", is going to give the idea that it's heavily frowned upon. When, in practice, I think an enterprise monorepo with many modules should definitely check in a go.work file. They just need to be mindful of the pitfalls.

Comment From: matloob

How about the following? I softened up the rare cases wording because I don't have visibility into how modules are structured in work scenarios but I want to be really careful about not recommending go.work files be checked in.

In general, it is recommended that go.work files not be checked into version control systems. Module developers should think carefully before checking in go.work files because of the considerable disadvantages- among others: * Checked-in go.work files might override developers' own go.work files contained in parent directories and cause confusion when the developers' go.work files don't apply. * Checked-in go.work files may also result in the wrong versions of modules being tested in continuous integration systems: go.work will override the versions of dependency modules that are in workspaces so those versions will not be respected in testing.

With that said there are some cases where it might make sense to do so: particularly if the workspace modules don't have any other modules that depend on them.

Comment From: Emptyless

The gosec tool can only scan multi module projects with a go.work file. I've tried to support scanning without go.work file checked into VCS but this is closed until there is consensus on go.work (https://github.com/securego/gosec/pull/1100). I just want to chime in that the wording doesn't necessarily have to be a recommendation but just acknowledge that there are two ways to deal with the go.work file and describe how to identify what would work for your case. Continuing on @matloob, how about:

Depending on the project, think of the following when deciding to add the go.work file to a version control system: - Are there modules external to the project that depend on the workspace modules? - Are the developers of the project aware that if they have a go.work file, it might be overruled by the project go.work if added? - Are the continuous integration pipelines configured such that the versions specified in go.mod files are respected, e.g. with a GOWORK=off environment variable?

About a monorepo, does it generally make sense to commit the go.work file? Maybe I'm interpreting incorrectly but isn't one of the benefits of using multiple modules in a large monorepo that it decouples them? Modules can publish new (possibly breaking) versions without having to refactor the entire monorepo. This decoupling would break if committing a global go.work listing all modules.

Comment From: matloob

@Emptyless I'm very surprised looking at the comments on your review. We should certainly not force people who work in multi module repositories to check in their go.work files and should support users working without checked-in go.work files. That's how you get the best experience with go.work files. go.work files are meant to be a convenience feature to make certain workflows easier- not a requirement.

I tried to hit the tone in my proposed wording that in some cases it is okay to check in go.work files but I certainly don't want to encourage doing so or even worse require it.

The best experience for a monorepo is to have a single module. Because there are cases where that's not possible, for example because different components of a monorepo are distributed and versioned separately, but that's exactly the decoupling that you mentioned.

Comment From: dylan-bourque

The best experience for a monorepo is to have a single module.

I would think exactly the opposite. In many (most? nearly every?) cases a monorepo will contain multiple things and those things would typically be versioned independently (pretty much the definition of a Go module). Having a monorepo be a single Go module feels like the opposite of what I would want/expect.

I use VS Code and it does in fact require that you have a go.work in place if your workspace contains multiple modules so I think it very much makes sense to either 1) commit go.work/go.sum and accept the trade-offs or 2) add scripts or other developer tooling to create and populate a go.work for the repo from scratch.

At work we've settled on option 2 for repos with >1 module, which are becoming more common for us.

Comment From: eliben

How about the following? I softened up the rare cases wording because I don't have visibility into how modules are structured in work scenarios but I want to be really careful about not recommending go.work files be checked in.

In general, it is recommended that go.work files not be checked into version control systems. Module developers should think carefully before checking in go.work files because of the considerable disadvantages- among others:

* Checked-in `go.work` files might override developers' own `go.work` files contained
  in parent directories and cause confusion when the developers' `go.work` files don't apply.

* Checked-in `go.work` files may also result in the wrong versions of modules being tested in continuous integration
  systems: `go.work` will override the versions of dependency modules that are in workspaces so those
  versions will not be respected in testing.

With that said there are some cases where it might make sense to do so: particularly if the workspace modules don't have any other modules that depend on them.

@matloob did you get any significant objections to this? If not, can this be "implemented" (by mentioning in the right docs)?

Comment From: matloob

I haven't gotten any significant objections. I'll send a CL to add this to ref/mod shortly.

Comment From: gopherbot

Change https://go.dev/cl/580115 mentions this issue: ref/mod: add a warning against checking in go.work files

Comment From: troy0820

Would it be useful, because there is a tutorial on how to use workspaces and the workflow that it represents, to provide a tutorial/workflow where checking the go.work would be beneficial? The use case that say kubernetes/kubernetes has is a lot different than most and that clarity would be beneficial when making decisions like this beyond the understanding of the disadvantages of it all.