This is the second post on the Go module series, which highlights Grab’s experience working with Go modules in a multi-module monorepo. In this article, we’ll focus on suggested solutions for catching unexpected changes to the
go.mod file and addressing dependency issues. We’ll also cover automatic upgrades and other learnings uncovered from the initial obstacles in using Go modules.
Vendoring Process Issues
Our previous vendoring process fell solely on the developer who wanted to add or update a dependency. However, it was often the case that the developer came across many unexpected changes due to previous vendoring attempts, accidental imports and changes to dependencies.
The developer would then have to resolve these issues before being able to make a change, costing time and causing frustration with the process. It became clear that it wasn’t practical to expect the developer to catch all of the potential issues while vendoring, especially since Go modules itself was new and still in development.
Avoiding Unexpected Changes
Reluctantly, we added a check to our CI process which ran on every merge request. This helped ensure that there are no unexpected changes required to go mod. This added time to every build and often flagged a failure, but it saved a lot of post-merge hassle. We then realised that we should have done this from the beginning.
Since we hadn’t enabled Go modules for builds yet, we couldn’t rely on the
\mod=readonly flag. We implemented the check by running
go mod vendor and then checking the resulting difference.
If there were any changes to
go.mod or the vendor directory, the merge request would get rejected. This worked well in ensuring the integrity of our
Roadblocks and Learnings
However, as this was the first time we were using Go modules on our CI system, it uncovered some more problems.
Private Repository Access
There was the problem of accessing private repositories. We had to ensure that the CI system was able to clone all of our private repositories as well as the main monorepo, by adding the relevant SSH deploy keys to the repository.
The check sometimes fired
false positives - detecting a go mod failure when there were no changes. This was often due to network issues, especially when the modules are hosted by less reliable third-party servers. This is somewhat solved in Go 1.13 onwards with the introduction of proxy servers, but our workaround was simply to retry the command several times.
We also avoided adding dependencies hosted by a domain that we haven’t seen before, unless absolutely necessary.
Inconsistent Go Versions
We found several inconsistencies between Go versions - running go mod vendor on one Go version gave different results to another. One example was a change to the checksums. These inconsistencies are less common now, but still remain between Go 1.12 and later versions. The only solution is to stick to a single version when running the vendoring process.
There are benefits to using Go modules for vendoring. It’s faster than previous solutions, better supported by the community and part of the language, so it doesn’t require any extra tools or wrappers to use it.
One of the most useful benefits from using Go modules is that it enables automated upgrades of dependencies in the go.mod file - and it becomes more useful as more third-party modules adopt Go modules and semantic versioning.
We call our solution for automating updates at Grab the AutoVend Bot. It is built around a single Go command,
go list -m -u all, which finds and lists available updates to the dependencies listed in
\json for JSON output). We integrated the bot with our development workflow and change-request system to take the output from this command and create merge requests automatically, one per update.
Once the merge request is approved (by a human, after verifying the test results), the bot would push the change. We have hundreds of dependencies in our main monorepo module, so we’ve scheduled it to run a small number each day so we’re not overwhelmed.
By reducing the manual effort required to update dependencies to almost nothing, we have been able to apply hundreds of updates to our dependencies, and ensure our most critical dependencies are on the latest version. This not only helps keep our dependencies free from bugs and security flaws, but it makes future updates far easier and less impactful by reducing the set of changes needed.
Using Go modules for vendoring has given us valuable and low-risk exposure to the feature. We have been able to detect and solve issues early, without affecting our regular builds, and develop tooling that’ll help us in future.
Although Go modules is part of the standard Go toolchain, it shouldn’t be viewed as a complete off the shelf solution that can be dropped into a codebase, especially a monorepo.
Like many other Go tools, the Modules feature comprises many small, focused tools that work best when combined together with other code. By embracing this concept and leveraging things like go list, go mod graph and go mod vendor, Go modules can be made to integrate into existing workflows, and deliver the benefits of structured versioning and reproducible builds.
I hope you have enjoyed this article on using Go modules and vendoring within a monorepo.
Grab is a leading superapp in Southeast Asia, providing everyday services that matter to consumers. More than just a ride-hailing and food delivery app, Grab offers a wide range of on-demand services in the region, including mobility, food, package and grocery delivery services, mobile payments, and financial services across over 400 cities in eight countries.
Powered by technology and driven by heart, our mission is to drive Southeast Asia forward by creating economic empowerment for everyone. If this mission speaks to you, join our team today!
The cute Go gopher logo for this blog’s cover image was inspired by Renee French’s original work.