go-version: SemVer comparison where WithPrefix quietly drops the prefix from comparisons
A Go (golang) library for parsing and verifying versions and version constraints.
At a glance
- What is it?
- go-version parses, compares, sorts and constrains SemVer strings in Go, with no dependencies and a go.mod that declares go 1.16. The one behaviour worth reading twice is WithPrefix: the prefix is stripped for comparison, so two versions from different prefixes compare as if the prefixes were never there.
- Who is it for?
- Adopt go-version when you need SemVer ordering rather than lexicographic string ordering, especially for prerelease handling and sorting a collection of versions, since that is the whole library and it has no dependencies. Do not use WithPrefix as if it scoped comparisons to one prefix, because the README states the prefix is not part of the canonical value and cross-prefix comparisons ignore it.
- Can I use it commercially?
- Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The library is four files and no dependencies
go-version is a HashiCorp library for parsing versions and version constraints and verifying versions against constraints. It sorts a collection of versions properly, handles prerelease and beta versions, can increment versions, and its one hard requirement is that versions must follow SemVer.
The repository is unusually small for a HashiCorp project. The top level is constraint.go, constraint_test.go, version.go, version_collection.go, version_collection_test.go and version_test.go, plus go.mod, a Makefile, a LICENSE, a CHANGELOG.md and .copywrite.hcl. go.mod declares module github.com/hashicorp/go-version and nothing else: no require block, and a go directive of 1.16.
That last detail is the practical headline. A library with a 1.16 language directive drops into almost any Go module without forcing a toolchain upgrade, and with an empty require block it cannot drag anything into your dependency graph. For a function that gets called in a version comparison on a hot path, that is a reasonable default, and it is also why there is no configuration surface to document.
NewVersion normalises a loose string, then Compare does the work
Parsing is done with NewVersion, and the examples show it accepting short forms. "1.2" parses, and so does "1.5+metadata", which means build metadata is tolerated and the core version can omit the patch level.
Comparison has several shapes. LessThan is the one in the example, alongside GreaterThan, Equal, and a plain Compare that returns an int so you can use >=, <= and the rest directly:
v1, err := version.NewVersion("1.2")
v2, err := version.NewVersion("1.5+metadata")
// Comparison example. There is also GreaterThan, Equal, and just
// a simple Compare that returns an int allowing easy >=, <=, etc.
if v1.LessThan(v2) {
fmt.Printf("%s is less than %s", v1, v2)
}Both calls return an error, and the example discards it. For input that comes from a file, a config value or an API response, that error is the only signal that the string is a version at all, and ignoring it means a parse failure produces a nil version rather than a rejected input.
That is the habit to change when using this library: NewVersion returns a version and an error, and the second return value is where malformed input announces itself.
WithPrefix strips the prefix, and comparison will not tell you it did
The WithPrefix option lets you parse versions that carry a known release prefix such as `deployment-` or `controller-`. The prefix is stripped before parsing, and the README is explicit about the consequence in a paragraph that deserves to be read twice.
After parsing, the prefix is not part of the canonical version value. The regular comparison methods, Compare, LessThan, Equal and GreaterThan, compare only the stripped version. If you compare versions from different prefixes with these methods, the prefixes are ignored.
The example makes the shape obvious, because the print statement calls Original() to recover what was actually in the input:
v1, _ := version.NewVersion("deployment-v1.2.3-beta+metadata", version.WithPrefix("deployment-"))
v2, _ := version.NewVersion("deployment-v1.2.4", version.WithPrefix("deployment-"))
if v1.LessThan(v2) {
fmt.Printf("%s (%s) is less than %s (%s)\n", v1, v1.Original(), v2, v2.Original())
// Outputs: 1.2.3-beta+metadata (deployment-v1.2.3-beta+metadata) is less than 1.2.4 (deployment-v1.2.4)
}The parsed value prints as 1.2.3-beta+metadata and Original() prints the deployment- prefixed string. So the canonical form has lost the prefix, and a comparison between a deployment version and a controller version at the same version number returns equal.
The README states the remedy rather than leaving it implicit: if you need to reject cross-prefix comparisons, inspect the parsed prefixes before comparing. Do that yourself; the comparison methods will not do it.
Constraints are comma-separated expressions checked against one version
Constraints are built with NewConstraint from a string expression, and checked with Check:
v1, err := version.NewVersion("1.2")
// Constraints example.
constraints, err := version.NewConstraint(">= 1.0, < 1.4")
if constraints.Check(v1) {
fmt.Printf("%s satisfies constraints %s", v1, constraints)
}The expression is a comma-separated conjunction, so ">= 1.0, < 1.4" means at least 1.0 and less than 1.4, and 1.2 satisfies it. Note the space after the operator, which is how the documented examples write it.
Both NewVersion and NewConstraint return errors here, and both are discarded in the examples. A constraint string that fails to parse produces an unusable object, so the parse error is the only signal that a configuration file contains a malformed constraint.
Because Check takes a single version, checking a set of installed versions against one constraint expression is a loop on your side. The library does not provide a function that answers whether a set of versions is mutually satisfiable or which one is the highest match.
Sorting is the feature that replaces naive string ordering
Sorting is where version handling usually goes wrong, and version_collection.go exists for it. The pattern is to parse each string and hand the slice to a Collection:
versionsRaw := []string{"1.1", "0.7.1", "1.4-beta", "1.4", "2"}
versions := make([]*version.Version, len(versionsRaw))
for i, raw := range versionsRaw {
v, _ := version.NewVersion(raw)
versions[i] = v
}
// After this, the versions are properly sorted
sort.Sort(version.Collection(versions))That input is a demonstration of the problem. Sorted as strings, "0.7.1" sorts above "1.1" because of the character comparison, and "1.4-beta" lands next to "1.4" by accident. Sorted by Collection, the ordering follows SemVer precedence, so the prerelease comes before its release and 0.7.1 comes before 1.1.
Collection implements sort.Interface over a slice of *Version, so it drops into sort.Sort directly and also into anything else expecting that interface. The parse loop is on you, including handling the error.
If you are writing sort.Strings over version strings anywhere in your code, this is the type that replaces it.
A dependency-pinned toolchain and a yearly minor bump
The Makefile shows how the project enforces its own standards, and the pinning is the notable part. The deps target installs tools by commit hash rather than by version tag:
@go install github.com/hashicorp/copywrite@b3e6599f43beff698f471c6f46888045453fa030 # v0.25.3
@go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5 # v2.12.2The comment records which release that hash corresponds to, so a reader can tell what they are getting while the build stays reproducible. copywrite manages the licence headers, which is why.copywrite.hcl is at the repository root.
The default target runs copywrite headers --plan, then golangci-lint run, then tests. The test invocation is `go test -v -race -timeout=60s./...`, with the race detector and a 60 second timeout, on a library with no concurrency of its own. That is a conservative default rather than a busy one.
Releases are steady. v1.7.0 shipped on 2024-05-24, v1.8.0 on 2025-11-28 and v1.9.0 on 2026-03-30, and the last push was on 2026-10-16. A minor bump roughly every year is a good sign for a library this widely used, and the changelog is in the repository.
The licence is MPL-2.0, which is weak copyleft: modifying the library itself obliges you to publish those modifications, while using it in your own code does not. That is the right fit for a library in this position.
Where go-version is the wrong tool
The clearest wrong-tool case is input that is not SemVer. The README states that versions used with go-version must follow SemVer, and there is no lenient mode for vendor strings, calendar dates in a different format, or product names embedded in the version. Parsing such input fails, and a system that must accept several formats needs to normalise them before this library sees them.
The second is cross-prefix identity. If your input strings are deployment-v1.2.3 and controller-v1.2.3 and you need to know they are different artefacts, WithPrefix will not give you that, because the prefix is gone from the canonical value and comparisons ignore it. You have to inspect prefixes yourself, as the README says.
The third is version incrementing as a workflow. The overview mentions that the library can increment versions, which is useful, but for a release pipeline that needs to bump across multiple segments with metadata rules, that is a small feature inside a comparison library rather than a release tool.
The fourth is a dependency you cannot have. Zero dependencies is a feature here, and it is also why the library is so small. If you are already pulling in another semantic version library through another tool, having both in the graph is a choice to make deliberately rather than by accident.
Editorial conclusion
Adopt go-version when you need SemVer ordering rather than lexicographic string ordering, especially for prerelease handling and sorting a collection of versions, since that is the whole library and it has no dependencies. Do not use WithPrefix as if it scoped comparisons to one prefix, because the README states the prefix is not part of the canonical value and cross-prefix comparisons ignore it. Verify first whether your inputs actually follow SemVer, since versions used with this library must, and inspect the parsed prefixes yourself if you need to reject comparisons across different prefixes.
Frequently asked questions
How do I install go-version?
With `go get github.com/hashicorp/go-version`. The module has no dependencies in its go.mod and declares go 1.16, so it drops into an existing module without pulling anything else in.
How does the WithPrefix option affect version comparison?
The prefix is stripped before parsing and is not part of the canonical value, so Compare, LessThan, Equal and GreaterThan compare only the stripped version. Versions from different prefixes therefore compare as if the prefixes were absent, and the README says to inspect the parsed prefixes yourself if you need to reject those comparisons.
How do I check a version against a constraint in go-version?
Build the constraint with NewConstraint from a comma-separated expression such as ">= 1.0, < 1.4", then call Check on a parsed version. Both constructors return an error, which the README examples discard.
How do I sort a slice of version strings correctly in Go?
Parse each string with NewVersion into a slice of *version.Version and sort it with sort.Sort(version.Collection(versions)), which orders by SemVer precedence rather than lexicographically. The README uses the input {"1.1", "0.7.1", "1.4-beta", "1.4", "2"} to demonstrate the difference.
What license does go-version use?
MPL-2.0, with a LICENSE file at the repository root and licence headers managed by the copywrite tool configured through .copywrite.hcl.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/hashicorp-go-version)Was this useful?